ios灵动岛电商秒杀开发实践(代码片段)

兵临城下也 兵临城下也     2022-12-02     637

关键词:

一、基本概述

名词基础知识

苹果在 iPhone 14 Pro 系列中增加一个灵动岛,主要目的是隐藏挖孔造型的高端“感叹号屏”。 通过动画的视觉差异,用户找不到原来的挖孔屏。灵动岛是一种巧妙的设计,模糊了软件和硬件之间的界限,它可以在锁屏的情况下根据不同的应用程序操作和提示、通知和活动内容的需求,自动改变大小和形状,展示用户关注的事情。


开发基础知识

1、设备只支持iPhone,并且是有“药丸屏”的iPhone14Pro和14Pro Max上;

2、Max系统版本、编译器及iOS系统版本:>=MacOS12.4、>=Xcode14.0+beta4、>=iOS16.1+beta;

3、使用 ActivityKit 用于配置、开始、更新、结束实现 Live Activity 能力。使用 WidgetKitSwiftUIwidget 小组件中创建 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.swift

import Foundation
import ActivityKit

struct SeckillProductAttributes: ActivityAttributes 
    typealias SeckillProductState = ContentState
    public struct ContentState: Codable, Hashable 
	      // 可变的属性需要放在这里,activity调用update进行数据的更新
        var isSeckill: Bool
    
    
    // 一个灵动岛任务的初始化数据,描述一些不可变数据
    let productId: String
    let name: String
    let price: String
    let image: String
    let countDown: Double
    let isSeckill: Bool
    
    init(productId: String, name: String,  price: String, image: String, countDown: Double, isSeckill: Bool = false) 
        self.productId = productId
        self.name = name
        self.price = price
        self.image = image
        self.countDown = countDown
        self.isSeckill = isSeckill
    

灵动岛布局(Widget工程):继承 Widget ,通过 ActivityConfiguration 来管理锁屏及灵动岛的布局。

这里包含了如何从主工程获取数据展示及将点击事件传递给主工程的代码示例:

// HDSeckillAvtivityLiveActivity.swift

import ActivityKit
import WidgetKit
import SwiftUI

struct 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的Icon
                Image("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的id
    static 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] = activityId
        UserDefaults.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 
    @discardableResult
    public static func activityAction(url: URL) -> Bool 
        let host = url.host
        guard host != nil else  return false 
        let queryItems = URLComponents(string: url.absoluteString)?.queryItems
        guard 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.swift

override 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[安卓手机也用上了“灵动岛”?还有人开发出了“灵动鸟”

自从iPhone14系列发布以来,最让人印象深刻的莫过于灵动岛设计。其独特的软硬件交互体验不仅吸引了很多消费者为其买单,还有不少网友脑洞大开,为其赋予了更多的应用场景。由于灵动岛只能在iPhone14Pro系列上被... 查看详情

springboot构建电商基础秒杀项目总结-交易模块开发(代码片段)

交易模块开发1、创建数据库CREATETABLE`order_info`(`id`varchar(32)CHARACTERSETutf8COLLATEutf8_binNOTNULL,`user_id`int(11)NOTNULLDEFAULT0,`item_id`int(11)NOTNULLDEFAULT0,`item_pri 查看详情

一个基于ssm框架开发的高并发电商秒杀web系统(代码片段)

0前言一个基于SSM框架的高并发秒杀系统采用IDEA+Maven+SSM+Mysql+Redis+Jetty、Bootstrap/Jquery开发。通过这个小项目,理清了基于SSM框架开发Web应用的流程以及常见的避坑方法,并在最后简单采用了Redis缓存以及MysqlProcedure对项目进行了高... 查看详情

大型车祸现场,电商秒杀超卖,这个锅到底有谁来背?(代码片段)

背景小明在一家在线购物商城工作,最近来了一个新需求,需要他负责开发一个商品秒杀模块,而且需求很紧急,老板要求必须尽快上线。方案小明一开始是这么做的,直接用数据库锁进行控制,获取秒杀商品数量并加锁,如果... 查看详情

android版灵动岛插件上线;iphone15或将改名,并改用usb-c接口;​swift5.7发布|极客头条...(代码片段)

...#xff08;ID:CSDNnews)一分钟速览新闻点!Android版灵动岛插件上线史玉柱重返研发一线改游戏:建议《原始征途》公测延期中国信 查看详情

电商 秒杀系统 设计思路和实现方法(代码片段)

电商 秒杀系统 设计思路和实现方法2017年05月26日00:06:35阅读数:36621秒杀业务分析正常电子商务流程(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货秒杀业务的特性(1)低廉价格... 查看详情

解密秒杀系统架构,不是所有的系统都能做秒杀!(代码片段)

摘要:教你如何设计一个秒杀系统架构:从电商系统架构到秒杀系统、从高并发“黑科技”与致胜奇招到服务器硬件优化,全方位立体掌握秒杀系统架构!!本文分享自华为云社区《实践出真知:全网最强... 查看详情

秒杀系统——秒杀功能设计理念(代码片段)

文章目录电商系统下单功能概述订单量:从0到1000(架构1)订单量:从1000到100万锁机制悲观锁乐观锁分布式锁消息队列消息队列:架构2从电商系统到秒杀系统流量限制热门资源隔离最终:架构3总结1.秒杀... 查看详情

秒杀系统——秒杀功能设计理念(代码片段)

文章目录电商系统下单功能概述订单量:从0到1000(架构1)订单量:从1000到100万锁机制悲观锁乐观锁分布式锁消息队列消息队列:架构2从电商系统到秒杀系统流量限制热门资源隔离最终:架构3总结1.秒杀... 查看详情

解密redis助力双11背后电商秒杀系统(代码片段)

...n/EAlQqQD背景秒杀的特征秒杀系统背景秒杀活动是绝大部分电商选择的低价促销,推广品牌的方式。既可以给平台带来用户量,还可以提高平台知名度。一个好的秒杀系统,可以提高平台系统的稳定性和公平性,获... 查看详情

电商秒杀系统架构分析与实战!(代码片段)

点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识来源:https://my.oschina.net/xianggao/blog/5249431秒杀业务分析正常电子商务流程(1)查询商品; (2)创建订单; (3)... 查看详情

java用redis如何处理电商平台,秒杀抢购超卖(代码片段)

这是在一个方法调用下面代码的部分if(!this.checkSoldCountByRedisDate(key,limitCount,buyCount,endDate))//标注10: thrownewServiceException("您购买的商品【"+commodityTitle+"】,数量已达到活动限购量"); 2、下面是 查看详情

电商系统架构如何做秒杀?分析与实战来了!(代码片段)

点击上方关注“终端研发部”设为“星标”,和你一起掌握更多数据库知识来源:https://my.oschina.net/xianggao/blog/5249431秒杀业务分析正常电子商务流程(1)查询商品; (2)创建订单; (3)... 查看详情

如何设计一个秒杀系统(代码片段)

什么是秒杀秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大... 查看详情

苹果灵动岛将向第三方开放接口,微信、抖音可以“上岛”了吗?

...也非常火热,在iPhone14的发布会上,iPhone系列再次推出了灵动岛的体验。 iPhone推出的灵动岛体验可以说是此次系列的最大亮点,毕竟灵动岛对于手机领域来说是非常先进的一款模式。但是根据iPhone到现在的使用感受而言,灵... 查看详情

全流程开发go实战电商网站高并发秒杀系统

获取资源点击这里:全流程开发GO实战电商网站高并发秒杀系统第1章课程介绍【学前须知】本章对这门课程进行说明,包括:秒杀系统涉及模块的介绍,秒杀核心的知识点的介绍,课程的学习规划等。 1-1课程介绍试看第2章... 查看详情

实践出真知:全网最强秒杀系统架构解密,不是所有的秒杀都是秒杀!!(代码片段)

大家好,我是冰河~~很多小伙伴反馈说,高并发专题学了那么久,但是,在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段ÿ... 查看详情

实践出真知:全网最强秒杀系统架构解密,不是所有的秒杀都是秒杀!!(代码片段)

大家好,我是冰河~~很多小伙伴反馈说,高并发专题学了那么久,但是,在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段ÿ... 查看详情