前言:本文档主要实现2个功能
- 1.3D Touch 长按添加快捷键入口。
竖的红框内 - 2.3D Touch 长按展示小组件样式。
横的红框内
直接上图看一下效果:

041a5f78f0660a20359c42f979faf854.jpg
一. 我们先来实现上面第一个功能
- 我们现在info.plist文件添加key
Home Screen Shortcut Items
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconFile</key>
<string>fy_goHdNew</string>
<key>UIApplicationShortcutItemType</key>
<string>activity</string>
<key>UIApplicationShortcutItemTitle</key>
<string>去领券</string>
</dict>
<dict>
<key>UIApplicationShortcutItemIconFile</key>
<string>fy_dyGoZmNew</string>
<key>UIApplicationShortcutItemType</key>
<string>subscribe</string>
<key>UIApplicationShortcutItemTitle</key>
<string>去订阅</string>
</dict>
<dict>
<key>UIApplicationShortcutItemIconFile</key>
<string>fy_xcGoZmNew</string>
<key>UIApplicationShortcutItemType</key>
<string>itinerary</string>
<key>UIApplicationShortcutItemTitle</key>
<string>看行程</string>
</dict>
</array>
我们来解释一下字段含义:
UIApplicationShortcutItemIconFile 用来设置左侧图标展示
UIApplicationShortcutItemTitle 用来设置右侧名称展示
UIApplicationShortcutItemType用来判断跳转类型(页面)
备注:上面是通过pilt文件进行配置,也可以使用纯代码进行编写
接下来我们来写逻辑:
点击按钮时候分为热启动和冷启动,热启动就是app正在运行,回到桌面长按,冷启动就是app没有正在运行。我们直接上代码:
先来实现热启动:
在AppDelegate.h
@property (nonatomic, strong) UIApplicationShortcutItem *smallShortcutItem;
在AppDelegate.m处理点击逻辑
-(void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler
{
BOOL ok = [self handleShortcutItem:shortcutItem];
if (completionHandler) completionHandler(ok);
}
- (void)handlePendingShortcutIfNeeded {
if (!self.pendingShortcutItem) return;
UIApplicationShortcutItem *item = self.pendingShortcutItem;
BOOL isXf = [self handleShortcutItem:item];
if (isXf == YES) {
self.pendingShortcutItem = nil;
}
}
- (BOOL)handleShortcutItem:(UIApplicationShortcutItem *)item {
//获取根视图 根据你们自己的逻辑进行跳转
BaseTabBar *tab = (BaseTabBar
*)self.window.rootViewController;
if (![tab isKindOfClass:[BaseTabBar class]]) return NO;
//activity subscribe itinerary 就是你在info.plist文件设置的type:UIApplicationShortcutItemType
if ([item.type isEqualToString:@"activity"]) {
//跳转到领券
return YES;
} else if ([item.type isEqualToString:@"subscribe"]) {
//跳转到订阅
return YES;
} else if ([item.type isEqualToString:@"itinerary"]) {
//跳转到行程
return YES;
}
else if ([item.type isEqualToString:@"gorent"]) {
//跳转到首页
return YES;
}
return NO;
}
我们来实现冷启动:
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIApplicationShortcutItem *item =
launchOptions[UIApplicationLaunchOptionsShortcutItemKey];
if (item) {
self.pendingShortcutItem = item;
// 冷启动场景:稍后手动处理
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self handlePendingShortcutIfNeeded];
});
}
}
二. 我们先来实现上面第一个功能 (接下来我们来实现:3D Touch 长按展示小组件样式。横的红框内)
1.需要我们新建一个target,我的命名为:MapleTripSamllWidget
点击xcode导航栏File->New->Target 如下图:

a2d444786166b718a23b223a9ec68249.png
这个时候您的项目文件结构会多出一个文件夹,如下图:

5ea9f89fd86ee46017f1d4521daab6c2.png
我们需要配置一下Scheme URL, 在info.plist设置如下:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>tripWidgetquick</string>
</array>
<key>CFBundleURLName</key>
<string>tripWidgetquick</string>
</dict>
<array>
截图如下:

291556d25ad12bb8129e913b01d81d1e.png
在文件MapleTripSamllWidgetBundle.swift代码如下:
import WidgetKit
import SwiftUI
@main
struct MapleTripSamllWidgetBundle: WidgetBundle {
var body: some Widget {
MapleTripSamllWidget()
}
}
在文件MapleTripSamllWidget.swift代码如下:
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> Entry { Entry(date: Date()) }
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) { completion(Entry(date: Date())) }
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
completion(Timeline(entries: [Entry(date: Date())], policy: .after(Date().addingTimeInterval(1800))))
}
}
struct Entry: TimelineEntry { let date: Date }
enum WidgetRoute: String, CaseIterable {
case coupon = "coupon"
case subscribe = "subscribe"
case trip = "trip"
case order = "order"
var url: URL { URL(string: "tripWidgetquick://\(rawValue)")! }
}
private struct QuickItem {
let title: String
let icon: String
let route: WidgetRoute
}
private let quickItems: [QuickItem] = [
QuickItem(title: "去领券", icon: "trip_lingquan", route: .coupon),
QuickItem(title: "去租车", icon: "trip_zuche", route: .order),
QuickItem(title: "去订阅", icon: "trip_dingyue", route: .subscribe),
QuickItem(title: "看行程", icon: "trip_stroke", route: .trip)
]
private struct TaskCardView: View {
var body: some View {
HStack(spacing: 10) {
leftPanel
rightGrid
}
.padding(10)
.containerBackground(for: .widget) {
LinearGradient(
colors: [Color(red: 0.98, green: 0.86, blue: 0.78), Color(red: 0.96, green: 0.93, blue: 0.87)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}
private var leftPanel: some View {
VStack(alignment: .center, spacing: 6) {
HStack(spacing: 4) {
Image("trip_title")
.font(.system(size: 11, weight: .semibold))
.cornerRadius(4)
.foregroundColor(.clear)
Text("可以是你app的名字")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(Color(red: 0.25, green: 0.2, blue: 0.2))
}
.frame(width: 110, height: 12)
ZStack {
Text("可以写一下描述!")
.font(.system(size: 10, weight: .regular))
.foregroundColor(Color(.black))
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 8)
}
.frame(width: 110, height: 10)
ZStack {
Image("trip_actHd")
.resizable()
.foregroundStyle(.white, .clear)
.scaledToFit()
.frame(width: 80, height: 80, alignment: .center)
}
.frame(width: 80, height: 80)
Text("天天好礼送不停~")
.font(.system(size: 10, weight: .bold))
.foregroundColor(Color(.red))
.frame(maxWidth: .infinity, alignment: .center)
}
.frame(maxWidth: .infinity, alignment: .center)
}
private var rightGrid: some View {
VStack(spacing: 6) {
HStack(spacing: 6) {
quickCell(item: quickItems[0])
quickCell(item: quickItems[1])
}
HStack(spacing: 6) {
quickCell(item: quickItems[2])
quickCell(item: quickItems[3])
}
}
.frame(width: 125)
}
private func quickCell(item: QuickItem) -> some View {
Link(destination: item.route.url) {
VStack(spacing: 4) {
Image(item.icon)
.resizable()
.scaledToFit()
.font(.system(size: 17, weight: .medium))
.foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2))
Text(item.title)
.font(.system(size: 12, weight: .bold))
.foregroundColor(Color(red: 0.2, green: 0.2, blue: 0.2))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.white.opacity(0.55))
)
}
.buttonStyle(.plain)
}
}
@available(iOS 17.0, *)
struct MapleTripSamllWidgetEntryView: View {
var body: some View {
TaskCardView()
}
}
@available(iOS 17.0, *)
struct MapleTripSamllWidget: Widget {
let kind: String = "tripWidgetquick"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { _ in
MapleTripSamllWidgetEntryView()
}
.configurationDisplayName("快捷入口")
.description("任务卡片组件")
.supportedFamilies([.systemMedium])
}
}
MapleTripSamllWidget.swift这个文件是使用SwiftUI来进行布局展示的,那我来解释一下,这里面的重点代码:
- 第一个
enum WidgetRoute: String, CaseIterable {
case coupon = "coupon"
case subscribe = "subscribe"
case trip = "trip"
case order = "order"
var url: URL { URL(string: "tripWidgetquick://\(rawValue)")! }
}
//tripWidgetquick 这个值配置的要和info.plist文件设置成一样的,后面用来跳转判断使用。
- 第二个
@available(iOS 17.0, *)
struct MapleTripSamllWidget: Widget {
let kind: String = "tripWidgetquick"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { _ in
MapleTripSamllWidgetEntryView()
}
.configurationDisplayName("快捷入口")
.description("任务卡片组件")
.supportedFamilies([.systemMedium])
}
}
我们要知道,他有3种布局分别为systemSmall 小和systemMedium 中以及systemLarge 大,默认是支持3种布局,这样就需要对3种布局写3个样式,我这里面配置的是systemMedium,其他2中演示展示不出来,关键代码:
.supportedFamilies([.systemMedium])
上面配置完事之后,看截图:

IMG_1574 2.jpg
上面这张截图由于我在只配置了.supportedFamilies([.systemMedium])所以其他2中样式为灰色的且不可点击的。
下面这张截图是上面的截图点击红色框之后展示的样式:

IMG_1575 2.jpg
那么接下来还是处理点击去领券 去租车 去订阅 看行程事件逻辑处理。(也分为热启动和冷启动)
- 热启动处理:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*, id> *)options
{
NSString *string =[url absoluteString];
if ([string hasPrefix:@"tripWidgetquick://"]) {
return [self handleDeepLink:url];
}
}
- (BOOL)handleDeepLink:(NSURL *)url {
if (![[url.scheme lowercaseString] isEqualToString:@"tripwidgetquick"]) return NO;
NSString *route = url.host.lowercaseString ?: @"";
if ([route isEqualToString:@"coupon"]) {
//去领券
NSLog(@"去领券");
UIApplicationShortcutItem *itemShortcut = [[UIApplicationShortcutItem alloc] initWithType:@"activity" localizedTitle:@""];
[self handleShortcutItem:itemShortcut];
} else if ([route isEqualToString:@"subscribe"]) {
//去订阅
NSLog(@"去订阅");
UIApplicationShortcutItem *itemShortcut = [[UIApplicationShortcutItem alloc] initWithType:@"subscribe" localizedTitle:@""];
[self handleShortcutItem:itemShortcut];
} else if ([route isEqualToString:@"order"]) {
//去租车
NSLog(@"去租车");
UIApplicationShortcutItem *itemShortcut = [[UIApplicationShortcutItem alloc] initWithType:@"gorent" localizedTitle:@""];
[self handleShortcutItem:itemShortcut];
} else if ([route isEqualToString:@"trip"]) {
//看行程
NSLog(@"看行程");
UIApplicationShortcutItem *itemShortcut = [[UIApplicationShortcutItem alloc] initWithType:@"itinerary" localizedTitle:@""];
[self handleShortcutItem:itemShortcut];
}
return YES;
}
//handleShortcutItem 这个方法上面有。
- 冷启动处理:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSURL *launchURL = launchOptions[UIApplicationLaunchOptionsURLKey];
if (launchURL) {
// 冷启动场景:稍后手动处理
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self handleDeepLink:launchURL];
});
}
return YES;
}
大功告成~