iOS灵动岛【电商秒杀】开发实践
一、基本概述
名词基础知识
苹果在 iPhone 14 Pro 系列中增加一个灵动岛,主要目的是隐藏挖孔造型的高端“感叹号屏”。 通过动画的视觉差异,用户找不到原来的挖孔屏。灵动岛是一种巧妙的设计,模糊了软件和硬件之间的界限,它可以在锁屏的情况下根据不同的应用程序操作和提示、通知和活动内容的需求,自动改变大小和形状,展示用户关注的事情。
开发基础知识
1、设备只支持iPhone,并且是有“药丸屏”的iPhone14Pro和14Pro Max上;
2、Max系统版本、编译器及iOS系统版本:>=MacOS12.4、>=Xcode14.0+beta4、>=iOS16.1+beta;
3、使用
ActivityKit
用于配置、开始、更新、结束实现Live Activity
能力。使用WidgetKit
、SwiftUI
在widget
小组件中创建Live Activity
的用户界面,这样小组件和Live Activity
的代码是可以共享;4、
Live Activity
目前只能通过ActivityKit
从主工程获取数据,或者从 远程通知 获取最新数据;无法访问网络或者接受位置更新信息5、
ActivityKit
和 远程通知推送 更新的数据不能超过4KB;6、
Live Activity
可以给不同的控制绑定不同的 deeplink,使其跳转到不同的页面;7、
Live Activity
在用户主动结束前最多存活8小时;8、已经结束的
Live Activity
在锁屏也最多保留4小时,所以一个Live Activity
最长可以停留12小时;9、最多同时存在两组
Live Activity
,排列顺序待发现
二、项目构思
这里以电商常用的抢购商品作为实践,包含的交互方式主要有如下特征:
1、主工程商品能展示基本信息,包含图片、名称、价格、开抢时间、预约按钮等;
2、主工程记录商品预约状态及灵动岛的任务状态,避免重复预约;
3、开启预约后,在打开App的情况下,灵动岛看到该商品的基本信息,支持立即抢购;
包含了开发灵动岛的基础知识:
1、主工程数据通过 ActivityKit
来启动、更新、停止灵动岛 Widget;
2、灵动岛 Widget 的基本布局方式及开发注意事项;
3、灵动岛 Widget 点击跳转到主工程的事件通讯;
三、效果展示
四、代码实践
1、创建主工程及灵动岛Widget工程
在主工程的 info.plist
文件中添加 Supports Live Activities
并且设置为 YES
2、熟悉 ActivityKit
常用对象及API,创建基本视图
数据部分(主工程):继承 ActivityAttributes
,定义常用的数据来控制或改变UI及状态。这里包含的商品的基本信息,可以认为是不变的状态,可变的状态需要在 ContentState
中声明。
// SeckillProduct.swiftimport Foundation
import ActivityKitstruct SeckillProductAttributes: ActivityAttributes {typealias SeckillProductState = ContentStatepublic struct ContentState: Codable, Hashable {// 可变的属性需要放在这里,activity调用update进行数据的更新var isSeckill: Bool}// 一个灵动岛任务的初始化数据,描述一些不可变数据let productId: Stringlet name: Stringlet price: Stringlet image: Stringlet countDown: Doublelet isSeckill: Boolinit(productId: String, name: String, price: String, image: String, countDown: Double, isSeckill: Bool = false) {self.productId = productIdself.name = nameself.price = priceself.image = imageself.countDown = countDownself.isSeckill = isSeckill}
}
灵动岛布局(Widget工程):继承 Widget
,通过 ActivityConfiguration
来管理锁屏及灵动岛的布局。
这里包含了如何从主工程获取数据展示及将点击事件传递给主工程的代码示例:
// HDSeckillAvtivityLiveActivity.swiftimport ActivityKit
import WidgetKit
import SwiftUIstruct HDSeckillAvtivityLiveActivity: Widget {var body: some WidgetConfiguration {ActivityConfiguration(for: SeckillProductAttributes.self) { context in// 锁屏之后,显示的桌面通知栏位置,这里可以做相对复杂的布局VStack {Text("Hello").multilineTextAlignment(.center)}.activityBackgroundTint(Color.cyan).activitySystemActionForegroundColor(Color.black)} dynamicIsland: { context in// 灵动岛的布局代码DynamicIsland {/*这里是长按灵动岛区域后展开的UI有四个区域限制了布局,分别是左、右、中间(硬件下方)、底部区域这里采取左边为App的Icon、右边为上下布局(商品名称+商品图标)、中间为立即购买按钮,支持点击deeplink传参唤起App、底部为价格和倒计时区域*/DynamicIslandExpandedRegion(.leading) {Image("zyg100").resizable().frame(width: 32, height: 32)}DynamicIslandExpandedRegion(.trailing) {Text(context.attributes.name).font(.subheadline).multilineTextAlignment(.center)Spacer(minLength: 8)Image(systemName: context.attributes.image).multilineTextAlignment(.center)}DynamicIslandExpandedRegion(.center) {// 这里的url一定记得需要中文编码let url = "hdSeckill://seckill?productId=\(context.attributes.productId)&name=\(context.attributes.name)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!Link("立即购买", destination: URL(string: url)!).foregroundColor(.red).font(.system(size: 24, weight: .bold))}DynamicIslandExpandedRegion(.bottom) {VStack(alignment: .center, content: {Spacer(minLength: 8)Text("价格\(context.attributes.price)").font(.subheadline)Spacer(minLength: 8)Text(Date().addingTimeInterval(context.attributes.countDown * 60), style: .timer).font(.system(size: 16, weight: .semibold)).multilineTextAlignment(.center)}).foregroundColor(.green)}} compactLeading: {// 这里是灵动岛未被展开左边的布局,这里用来展示App的IconImage("zyg100").resizable().frame(width: 32, height: 32)} compactTrailing: {// 这里是灵动岛未被展开右边的布局,这里用来商品的名称HStack {Text(context.attributes.name).font(.subheadline)}} minimal: {// 这里是灵动岛有多个任务的情况下,展示优先级高的任务,位置在右边的一个圆圈区域// 这用户展示商品的图标Image(systemName: context.attributes.image)}// 点击整个区域,通过deeplink将数据传递给主工程,做相应的业务.widgetURL(URL(string: "http://www.apple.com")).keylineTint(Color.red)}}
}
3、在主工程做好基本布局及初始化商品数据
**(主工程)**布局部分因为demo展示,直接使用 storyboard
快速完成:
// ViewController.swift
private func initProducts() {let carId = "2022101101"let carIsSeckill = checkIsSeckill(productId: carId)let bicycleId = "2022101102"let bicycleIsSeckill = checkIsSeckill(productId: bicycleId)let basketballId = "2022101103"let basketballIsSeckill = checkIsSeckill(productId: basketballId)let car = SeckillProductAttributes(productId:carId, name: "Model Y", price: "29.8万", image: "car.side.air.circulate", countDown: 60, isSeckill: carIsSeckill)let bicycle = SeckillProductAttributes(productId:bicycleId, name: "永久自行车", price: "1200", image: "bicycle", countDown: 120, isSeckill: bicycleIsSeckill)let basketball = SeckillProductAttributes(productId:basketballId, name: "斯伯丁篮球", price: "340", image: "basketball", countDown: 150, isSeckill: basketballIsSeckill)products.append(car)products.append(bicycle)products.append(basketball)// 判断本地缓存和系统ActiviKit的任务数据来显示当前列表if carIsSeckill {seckillButton0.setTitle("已预约", for: .normal)}if bicycleIsSeckill {seckillButton1.setTitle("已预约", for: .normal)}if basketballIsSeckill {seckillButton2.setTitle("已预约", for: .normal)}}
点击事件,调用 ActivityKit
将数据传递给 灵动岛 Widget:
// ViewController.swift
@IBAction func seckillAction(_ sender: UIButton) {if sender.tag >= products.count {return}if !ActivityAuthorizationInfo().areActivitiesEnabled {logToTextView(log: "不支持灵动岛")return}let product = products[sender.tag]// 判断系统的activities是否还执行该商品的任务,只有是在执行中的,才进行取消操作if sender.titleLabel?.text == "已预约" {if let activityId = getSeckillActivityId(productId: product.productId) {for activity in Activity<SeckillProductAttributes>.activities where activity.id == activityId {logToTextView(log: "取消预约购买\(product.name)")Task {await activity.end(dismissalPolicy:.immediate)}sender.setTitle("预约抢购", for: .normal)}}return}logToTextView(log: "开始预约购买\(product.name)")do {// 初始化状态,ContentState是可变的对象let initState = SeckillProductAttributes.ContentState(isSeckill: true)// 初始化状态,这里是不变的数据let activity = try Activity.request(attributes: product, contentState: initState)logToTextView(log: "activityId: \(activity.id)")sender.setTitle("已预约", for: .normal)// 将商品id和活动id关联起来,方便查询及取消操作saveSeckillState(productId: product.productId, activityId: activity.id)} catch {}}
同步管理活动数据及系统activities的状态:
// ViewController.swift
extension ViewController {// 保留商品的预约状态,key是商品id,value是activity的idstatic let seckillProductIds = "com.harry.toolbardemo.seckillProductIds"private func checkIsSeckill(productId: String) -> Bool {if let ids = UserDefaults.standard.value(forKey: ViewController.seckillProductIds) as? [String: String] {// 本地缓存包含该商品ID,并且系统的Activity依旧存在if ids.keys.contains(productId) {for activity in Activity<SeckillProductAttributes>.activities where activity.id == ids[productId] {return true}}}return false}private func saveSeckillState(productId: String, activityId: String) {var ids = [String: String]()if let tempIds = UserDefaults.standard.value(forKey: ViewController.seckillProductIds) as? [String: String] {ids = tempIds}ids[productId] = activityIdUserDefaults.standard.set(ids, forKey: ViewController.seckillProductIds)}private func getSeckillActivityId(productId: String) -> String? {if let ids = UserDefaults.standard.value(forKey: ViewController.seckillProductIds) as? [String: String] {return ids[productId]}return nil}private func removeSeckillActivityId(productId: String) {if var ids = UserDefaults.standard.value(forKey: ViewController.seckillProductIds) as? [String: String] {ids.removeValue(forKey: productId)UserDefaults.standard.set(ids, forKey: ViewController.seckillProductIds)}}
}
4、解析灵动岛 Widget的数据,并且做相应的操作
**(主工程中)**外部唤起会执行到 SceneDelegate
或者 AppDelegate
的系统API中,这里以 SceneDelegate
为例说明:
// SceneDelegate.swift// 实现该方法,接收并且处理外部唤起的数据做相应的事件
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {if let url = URLContexts.first?.url {debugPrint("url: \(url)")if url.scheme == "hdSeckill" {// 通过scheme来区分灵动岛相关的数据ActivityBrigde.activityAction(url: url)}}
}// SeckillProduct.swift
// 解析灵动岛的传递数据,做相应的事件,这里通过通知给主工程的控制器执行相应任务
struct ActivityBrigde {@discardableResultpublic static func activityAction(url: URL) -> Bool {let host = url.hostguard host != nil else { return false }let queryItems = URLComponents(string: url.absoluteString)?.queryItemsguard let queryItems = queryItems else { return false }var productId : String?var name : String?for item in queryItems {// 获取商品id和名称if item.name == "productId" {productId = item.value}else if item.name == "name" {name = item.value}}guard let productId = productId else { return false }debugPrint("立即抢购[\(name ?? "")] \(productId)")let info = ["productId": productId,"name": name ?? ""]NotificationCenter.default.post(name: Notification.Name("liveActivityNotif"), object: nil, userInfo: info)return true}
}
(主工程中) 主控制器监听该通知,并且做相应的任务处理:
// ViewController.swiftoverride func viewDidLoad() {super.viewDidLoad()NotificationCenter.default.addObserver(self, selector: #selector(liveActivityNotif(notif:)), name: Notification.Name("liveActivityNotif"), object: nil)
}extension ViewController {@objc private func liveActivityNotif(notif: Notification) {if let userInfo = notif.userInfo {if let productId = userInfo["productId"] as? String, let name = userInfo["name"] as? String {logToTextView(log: "立即抢购[\(name)] \(productId) \n")}}}private func logToTextView(log: String) {debugPrint(log);logTextView.text.append("\(log) \n")logTextView.scrollRectToVisible(CGRect(x: 0, y: logTextView.contentSize.height - 10, width: logTextView.contentSize.width, height: 10), animated: true)}
}
五、小结
整体的开发成本还是很低,基本上按照后文有几篇优秀的博客操作即可完成属于自己想法的灵动岛。之前没有接触过 Widget
和 SwiftUI
开发的iOS同学也不用担心。相信在不同类型的应用中,大家肯定也能够找到属于自己的灵感给用户更高的体验感。
本文涉及到的代码地址为 HDSeckillDemo 欢迎大家
iOS灵动岛【电商秒杀】开发实践相关推荐
- ios一个app调起另一个app_电商app开发价格:制作一个电商app需要多少钱?
智能手机的发展,带动了各式各样手机app的市场,现在大家网购大多数都是通过电商app实现,再加上分销.配送等模式发展,自建电商app成为很多企业的选择,电商app开发成本大概多少?制作一个电商app需 ...
- 构建高并发高可用的电商平台架构实践 转载
2019独角兽企业重金招聘Python工程师标准>>> 构建高并发高可用的电商平台架构实践 转载 博客分类: java 架构 [-] 一 设计理念 空间换时间 多级缓存静态化 索引 ...
- 电商库存锁_解密 Redis 助力双 11 背后电商秒杀系统
作者:AlibabaCloud 来源:https://github.com/AlibabaCloudDocs/kvstore/blob/master/cn.zh-CN/最佳实践/使用%20Redis% ...
- 广告电商系统开发功能只订单处理
广告电商系统之订单处理模块 订单处理功能块:订单下单:订单组合付款:订单列表,订单状态,订单物流信息,订单确认,订单售后,订单评论 1.订单下单 通过购物车的筛选,确定出下单的产品,系统自动计算出订 ...
- 2022年电商系统开发平台测评体验
2022年有哪些电商系统开发平台?我们一起来看看吧! 来客推b2b2c,商联达b2b2c商城开源 北京商联达科技有限公司上海分公司是商联达科创旗下一家以电商软件开发为核心,为企业提供多方面整合的电子商 ...
- 电商秒杀系统设计思路和实现方法
1 秒杀业务分析 正常电子商务流程 (1)查询商品:(2)创建订单:(3)扣减库存:(4)更新订单:(5)付款:(6)卖家发货 秒杀业务的特性 (1)低廉价格:(2)大幅推广:(3)瞬时售空:(4)一 ...
- 【session】Java电商平台开发技能图谱,探秘双十一百万PV
您将学习 Java电商平台开发技能图谱,探秘双十一百万PV 分享内容 企业级Java开发技能图谱 从宏观到微观,Java到底怎么学 为了支撑双十一,我们对Java程序做了什么 你将认识 热爱分享的常乐 ...
- 电商秒杀 Springboot + Redis +RocketMq +Mysql
项目地址 https://gitee.com/noah2021/miaosha 转载,亲测可用! 测试用例 在下订单之前需要先发布对应的商品用于在Redis中生成口令避免大量请求导致服务器崩溃~~ 发 ...
- 万字好文,电商秒杀系统架构分析与实战
点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 后台回复"k8s",可领取k8s资料 1 秒杀业务分析 ...
- APICloud AVM多端开发 |《生鲜电商app开发》项目源码深度解析
此项目为生鲜电商app开发类应用,主要功能包括商品列表.商品详情展示.购物车.登录注册.个人中心等. 项目源码在 https://github.com/apicloudcom/avm-simple 仓 ...
最新文章
- Python基础04-数据类型:数字、布尔、字符串
- 与时间相关的java源码_Java 调整日期和时间
- 【es】ElasticSearch master 选举
- Visio—如何画虚线?
- Hibernate笔记①--myeclipse制动配置hibernate
- 计算机网络(第七版)思维导图
- synctoy 自动运行_安排SyncToy在Windows 7中使用Task Scheduler自动运行
- 《一年学完mit计算机课程》,用了这种学习方法,他一年学完了33门MIT计算机课程...
- 一维数组二维数组(排序 最大值 插入排序)
- 两电脑使用网线连接传文件方法
- windows10搭建私有云服务(使用webdav服务端和公网端口映射)
- Linux命令:Nginx的启动、停止与重启
- HEVC编码框架和main函数代码跟踪解读
- SQL yyyyMMdd转yyyy-MM-dd
- HBuilderX 核心插件安装提示:“插件XXX下载失败,请检查网络”问题的解决办法
- [蓝桥杯] 剪邮票 (Python 实现)
- 区块链惊现山寨万融链 怎样判断一个区块链项目的真假
- Sequence Model-week2编程题1-词向量的操作【余弦相似度 词类比 除偏词向量】
- 计算机高中竞赛自主招生,关于高中五大学科竞赛的问题都在这里,参加自主招生必看!...
- STL源码学习(2)-迭代器概念与traits编程技法