美文网首页
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