美文网首页
iOS Today App Extension 制作(看完这一篇

iOS Today App Extension 制作(看完这一篇

作者: JasonFive | 来源:发表于2019-12-26 15:57 被阅读0次

Widget 窗口组件主要是作用是展示一些即时信息,或者作为一个快捷入口或者快捷窗口在展示在通知栏中,它能提供一些事件操作,包括本地储存,网络请求,蓝牙等等操作;但是不支持键盘录入,并且不建议scroll操作,因为会跟系统相冲突,这里冲突不是说不能使用scrollview控件,是不建议使用scroll操作

一、创建 Today Extension

创建 Today Extension 有两种方式:
1、Xcode -> File -> New -> Target -> iOS -> Today Extension


craet 1.jpg

2、选择工程 -> TARGETS 下面有个“+”按钮 -> iOS -> Today Extension


creat 2.jpg
点击 Next 之后你会发现你的工程多了一个 DoorAssistantToday 文件夹
create 3.png
这里可以直接设置 MainInterface.storyborad 中 View 的高度为 110 ,这是widget展示视图未折叠状态的默认高度
二、删除 Today Extension

删除 Today Extension 当然不能直接删除 DoorAssistantToday 这个文件夹,这样删除是没有用的,需要到 工程 -> TARGETS -> 选择 DoorAssistantToday 工程 -> 右击 菜单栏选择删除


delete.png

然后点击 delete ,工程中的DoorAssistantToday文件夹自动删除了。然后如果在 Podfile 中如果还添加了第三方库,也将这个删除掉

target 'DoorAssistantToday' do
    use_frameworks!
    # 网络请求
    pod 'Alamofire'
    pod 'Moya'
    pod 'Moya/RxSwift'
    pod 'Moya/ReactiveSwift'
    pod 'Then'
    pod 'HandyJSON'
    pod 'SnapKit'
end
三、配置 Plist 文件

1、如果你不想使用storyborad 开发,想使用纯代码开发,就将Plist 文件中Extension 中的 “ NSExtensionMainStoryboard” 修改为 “ NSExtensionPrincipalClass”,“ MainInterface”修改为“ TodayViewController”
系统默认:

<dict>
        <key>NSExtensionMainStoryboard</key>
        <string>MainInterface</string>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.widget-extension</string>
    </dict>

纯代码

<dict>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.widget-extension</string>
        <key>NSExtensionPrincipalClass</key>
        <string>TodayViewController</string>
    </dict>

设置之后就是这个样子,但是我觉得没多大必要去修改这个地方,并且有些时候修改这里,删除storyboard会出现一些你想不到的状况😁


1577348483495.jpg

2、然后就是如果你想 widget 使用网络,蓝牙,再在Plist中配置蓝牙、网络权限就行了

四、生命周期、关键代码
   override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
      
        // 设置折叠还是展开
        // 设置展开才会展示,设置折叠无效,左上角不会出现按钮
        extensionContext?.widgetLargestAvailableDisplayMode = .expanded
    }

   // 展开、折叠 发生改变时回调
   func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
         if activeDisplayMode == .compact { // 折叠
           self.preferredContentSize = CGSize(width: maxSize.width, height: maxSize.height) // 高度随便设置,感觉、反正也无效
         } else {
           self.preferredContentSize = CGSize(width: maxSize.width, height: myHeight)
         }
     }

   //  数据获取
   //  官方建议你通过实现它的回调来获取数据
   func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
       // Perform any setup necessary in order to update the view.
       
       // If an error is encountered, use NCUpdateResult.Failed
       // If there's no update required, use NCUpdateResult.NoData
       // If there's an update, use NCUpdateResult.NewData
       
       loadNetwork()
       completionHandler(NCUpdateResult.newData)
   }

Widget 打开宿主App
先在 URL Types 中设置好 URL Schemes 标示

URL Schemes.jpg
    func openMainApp() {
        //  打开宿主App
        extensionContext?.open(URL(string: "JZDoorAssistantTest://")!, completionHandler: { (success) in
            print("打开成功")
        })
    }

然后再宿主App中

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        // option 1
        return true
    }
    
    // NOTE: 9.0以后使用新API接口
    func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
        // option 1
        return true
    }
五、Widget 和 宿主App数据通信和代码共享
1、数据通信

Widget 和 宿主App之间通信有两种方式:
1、通过UserDefaults
2、通过Core data ,Sqlite ,FileManager 等,但是一般用第一种就好了,因为主要是

因为Today Extension虽然是扩展程序,但是跟宿主App是两个独立的程序,不能相互使用沙盒,所以为了相互能够进行数据传递,需要做一些处理

1.在 Developer 中 Certificates,Identifiers&Profiles 中 添加一个 App Groups


certicicates1.jpg
certificates2.jpg

2.在 Identifiers 中 创建App ID 或者 Edit App ID 将 Today Extension App ID 和 宿主 App ID 都加入 App Groups


certificates3.jpg

3.在Xcode中绑定Today Extension 和 宿主 App ,操作完 1 - 6 stp 工程中就会多 result 1 和 result 2文件


Bind.jpg

4.此时就可以在宿主 App 中存储数据,在 Widget 中读取数据了

    /*  储存数据 */
    //  需要填写正确的Droup ID 储存数据
    let shared = UserDefaults.init(suiteName: "group.com.jianzi.extension")!
    shared.set(JZGlobalData.share.token, forKey: "AsisstantToken")
    shared.synchronize()
    /*  读取数据 */
    //  通过正确的Droup ID 读取数据
    let shared = UserDefaults.init(suiteName: "group.com.jianzi.extension")!
    let token = shared.value(forKey: "AsisstantToken")
    let dict = NSMutableDictionary()
    dict["token"] = token
    print(dict)

需要注意的一点是:
2019-12-26 14:34:37.191766+0800 测试 [4432:971950] [User Defaults] Couldn't read values in CFPrefsPlistSource<0x28182af00> (Domain: group.com.jianzi.extension, User: kCFPreferencesAnyUser, ByHost: Yes, Container: (null), Contents Need Refresh: Yes): Using kCFPreferencesAnyUser with a container is only allowed for System Containers, detaching from cfprefsd

可能会报这个错误而拿不到数据,网上有 解决办法 ,我只是删除了原有的app,重新安装就行了,目前还没多测;到此就可以进行数据通信了

2、共享代码、图片

共享代码,感觉代码之间的耦合性比较高,如果代码逻辑单一,不涉及到很多文件,就可以共享代码,如果一个文件中包含了其他文件,那么共享的这个代码文件中需要兼容共享其他文件,这样比较麻烦;共享代码,图片很简单,在创建文件时,勾选 支持 Widget 工程就行

share.jpg
还有就是如果有使用第三方并且不是通过 cocoapods 管理的,也可以通过这个共享,如果是通过 cocoapods 导入,则需要在 cocoapods中也要导入第三方才能共享,或者不用共享,直接使用,如果需要用到 桥接 也可以在桥接文件中先import 再在使用文件中import
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/Artsy/Specs.git'

platform :ios, '9.0'
inhibit_all_warnings!

target 'JZDoorAssistant' do 
    use_frameworks!
    # 网络请求
    pod 'Alamofire'
    pod 'Moya'
    pod 'Moya/RxSwift'
    pod 'Moya/ReactiveSwift'
    pod 'Then'
    pod 'HandyJSON'
    pod 'SnapKit'
end

target 'DoorAssistantToday' do
    use_frameworks!
    # 网络请求
    pod 'Alamofire'
    pod 'Moya'
    pod 'Moya/RxSwift'
    pod 'Moya/ReactiveSwift'
    pod 'Then'
    pod 'HandyJSON'
    pod 'SnapKit'
end
六、请求网络数据
import UIKit
import Moya
import HandyJSON

fileprivate let JSONERROR = "Failed to map data to JSON."
fileprivate let JSONERRORTIPS = "网络开小差,请稍后重试"

enum TodayNetAPI {

    // MARK:-  主页
    case home_list_house(dict: NSDictionary)
}

/* *** 请求配置 *** */
extension TodayNetAPI : TargetType {
    
    // MARK:- *服务器地址 *
    var baseURL: URL { return URL(string: "http://demo.cn")! }
    
    
    // MARK:- *请求具体配置* ****************************************
    var path : String {
        switch self {
        case .home_list_house: return "Tenant_api/api"
        }
    }
    
    
    // MARK:- *请求任务事件(附带上参数)* ****************************************
    var task : Task {
        
        let parmeters = NSMutableDictionary()
        switch self {
            
        case .home_list_house(let dict):
            parmeters["token"]          = dict["token"]
            parmeters["api_name"]       = "list_house"
        }
        print("请求参数: \(parmeters)")
        return .requestParameters(parameters: parmeters as! [String : Any], encoding: URLEncoding.default)
    }
    
    
    
    // MARK:- *请求类型*
    var method: Moya.Method { return .post }

    
    // MARK:- *这个就是做单元测试模拟的数据,只会在单元测试文件中有作用*
    var sampleData: Data { return "".data(using: String.Encoding.utf8)! }

    
    // MARK:- *请求头*
    var headers: [String : String]? {
       
        let dict = NSMutableDictionary()
        dict["Content-type"] = "application/x-www-form-urlencoded; charset=utf-8"
        //dict["token"] = JZMGlobalData.share.token
        return (dict as! [String : String])
    }

    
    // MARK:- *是否执行Alamofire验证(可不配置)*
    var validate: Bool {
        return false
    }
}




//  数据请求返回闭包
typealias ResponseCallBack = (NSError?,Any?) -> Void
//  操作结果闭包
typealias resultBack = (Any?) -> Void

struct TodayNetwork {
    
    static let provide = MoyaProvider<TodayNetAPI>()
    static func request(
        _ target: TodayNetAPI,
        
        // @escaping标明这个闭包是会“逃逸”,通俗点说就是这个闭包在函数执行完成之后才被调用
        response responseCallback:@escaping (ResponseCallBack)) {
        
        print("PathUrl: \(target.path)")
        
        provide.request(target) { (result) in
            switch result {
            case let .success(moyaResponse):
                do {
                    // 解析数据
                    let dictValue:NSDictionary = try moyaResponse.mapJSON() as! NSDictionary
                    
                    var code:Int = 0
                    if dictValue["code"] is Int {
                        code = dictValue["code"] as! Int
                    }else{
                        code = Int(dictValue["code"] as! String)!
                    }
                    guard code == 1  else {
                        
                        let msg = ((dictValue["msg"] as? String) == JSONERROR) ? JSONERRORTIPS : dictValue["msg"]
                        let error = NSError(domain: "获取数据成功", code: 0, userInfo: [NSLocalizedDescriptionKey:"\(msg ?? "")"])
                        responseCallback(error,nil)
                        
                        if Int(code) == -999 || Int(code) == 101 {
                            
                            DispatchQueue.main.async {
                                //JZMGlobalData.share.logOut()
                            }
                        }
                        return
                    }
                    responseCallback(nil,dictValue)
                }
                    //如果do里边的代码发生错误,比如,解析不了数据,就会执行catch里边的代码
                catch let error {
                    // 暂时未配置,还不知道结果怎么样
                    //responseCallback(error as NSError,nil)
                    var errorMsg = error.localizedDescription
                    errorMsg = errorMsg.contains(JSONERROR) ==  true ? JSONERRORTIPS :  errorMsg
                    let err = NSError(domain: JSONERRORTIPS, code: 0, userInfo: [NSLocalizedDescriptionKey:errorMsg])
                    responseCallback(err,nil)
                }
                
            case let .failure(error):
                debugPrint(error)
                responseCallback(error as NSError,nil)
            }
        }
    }
}

然后在使用

    //  MARK:- 获取门禁列表
    func getHouseLockList(dict:NSDictionary,resultBack:@escaping resultBack) {
   
        TodayNetwork.request(.home_list_house(dict: dict)) { (error, data) in
            guard error == nil else { return }
            let dataDict = (data as! NSDictionary)
            let models = [LockModel].deserialize(from: (dataDict["data"] as! NSArray))
            resultBack(models)
        }
    }

这个地方我没有跟宿主App公用一个网络请求,为的是免得后面耦合性太高,不好维护,所以直接单独写了个,作为学习用;后面有什么问题我再继续修正,欢迎大家来学习交流

其中参考了几个文章也分享出来:
iOS Today Extension 入门指南
iOS Today Extensions 开发
iOS应用扩展(APP Extension)- Today Extension使用

相关文章

网友评论

      本文标题:iOS Today App Extension 制作(看完这一篇

      本文链接:https://www.haomeiwen.com/subject/pdzkoctx.html