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 标示
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 工程就行
还有就是如果有使用第三方并且不是通过 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使用
网友评论