版本记录
版本号 | 时间 |
---|---|
V1.0 | 2022.04.05 星期二 清明节 |
前言
3D Touch
是iPhone 6s+
,iOS9+
之后新增的功能。其最大的好处在于不启动app的情况下,快速进入app中的指定界面,说白了,就是一个快捷入口。接下来几篇我们就一起看下相关的内容。感兴趣的可以看下面几篇文章。
1. 3D Touch (一) —— Home Screen Quick Actions for SwiftUI App(一)
源码
首先看下工程组织结构
下面看下源码了
1. AppMain.swift
import SwiftUI
// MARK: App Main
@main
struct AppMain: App {
// MARK: Properties
private let actionService = ActionService.shared
private let noteStore = NoteStore.shared
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Body
var body: some Scene {
WindowGroup {
NoteList()
.environmentObject(actionService)
.environmentObject(noteStore)
.environment(\.managedObjectContext, noteStore.container.viewContext)
}
}
}
2. AppDelegate.swift
// 1
import UIKit
// 2
class AppDelegate: NSObject, UIApplicationDelegate {
private let actionService = ActionService.shared
// 3
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
// 4
if let shortcutItem = options.shortcutItem {
actionService.action = Action(shortcutItem: shortcutItem)
}
// 5
let configuration = UISceneConfiguration(
name: connectingSceneSession.configuration.name,
sessionRole: connectingSceneSession.role
)
configuration.delegateClass = SceneDelegate.self
return configuration
}
}
// 6
class SceneDelegate: NSObject, UIWindowSceneDelegate {
private let actionService = ActionService.shared
// 7
func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
// 8
actionService.action = Action(shortcutItem: shortcutItem)
completionHandler(true)
}
}
3. Note.swift
import Foundation
import CoreData
import UIKit
/// Extension of the generated `Note` Core Data model with added convenience.
extension Note {
var wrappedTitle: String {
get { title ?? "" }
set { title = newValue }
}
var wrappedBody: String {
get { body ?? "" }
set { body = newValue }
}
var identifier: String {
objectID.uriRepresentation().absoluteString
}
public override func willChangeValue(forKey key: String) {
super.willChangeValue(forKey: key)
// Helper to keep lastModified up-to-date when other properties are modified
if key == "title" || key == "body" || key == "isFavorite" {
lastModified = Date()
}
}
// 1
var shortcutItem: UIApplicationShortcutItem? {
// 2
guard !wrappedTitle.isEmpty || !wrappedBody.isEmpty else { return nil }
// 3
return UIApplicationShortcutItem(
type: ActionType.editNote.rawValue,
localizedTitle: "Edit Note",
localizedSubtitle: wrappedTitle.isEmpty ? wrappedBody : wrappedTitle,
icon: .init(systemImageName: isFavorite ? "star" : "pencil"),
userInfo: [
"NoteID": identifier as NSString
]
)
}
}
4. NoteStore.swift
import CoreData
import Foundation
class NoteStore: ObservableObject {
/// A singleton instance for use within the app
static let shared = NoteStore()
/// A test configuration for SwiftUI previews
static func preview() -> NoteStore {
return NoteStore(inMemory: true)
}
/// Storage for Core Data
let container = NSPersistentContainer(name: "NoteBuddy")
private init(inMemory: Bool = false) {
// Use an in-memory store if required (the default is persisted)
if inMemory {
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
}
// Attempt to load the persistent stores
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Failed to load persistent store: \(error)")
}
}
// If there is no content (i.e first use or it was deleted), populate with sample data
do {
if try container.viewContext.count(for: Note.fetchRequest()) == 0 {
try createDefaultData()
}
} catch {
fatalError("Failed to create initial sample data: \(error)")
}
}
/// Helper for saving changes only when requred
func saveIfNeeded() {
guard container.viewContext.hasChanges else { return }
try? container.viewContext.save()
}
/// Helper method for creating a new note
func createNewNote() -> Note {
let note = Note(context: container.viewContext)
note.title = "My Note"
note.body = "This is my new note."
note.isFavorite = false
note.lastModified = Date()
try? container.viewContext.save()
return note
}
/// Helper method for finding a `Note` with the given identifier
func findNote(withIdentifier id: String) -> Note? {
guard
let uri = URL(string: id),
let objectID = container.persistentStoreCoordinator.managedObjectID(forURIRepresentation: uri),
let note = container.viewContext.object(with: objectID) as? Note
else { return nil }
return note
}
/// Private helper method for inserting demo note data into the main context
private func createDefaultData() throws {
let pumkinPie = Note(context: container.viewContext)
pumkinPie.title = "Pumpkin Pie"
pumkinPie.body = "For this recipe you will need some flour, butter, pumpkin, sugar, and spices."
pumkinPie.isFavorite = true
pumkinPie.lastModified = Date()
let groceries = Note(context: container.viewContext)
groceries.title = "Groceries"
groceries.body = "1x Flour\n2x Bananas\n1x Tomato paste\n3x Oranges\n5x Onions"
groceries.lastModified = Date()
let newLaptop = Note(context: container.viewContext)
newLaptop.title = ""
newLaptop.body = [
"With the new, M1-powered MacBook Pro laptops, I've been thinking about what ",
"option is best for working with Xcode and doing some video editing. Perhaps ",
"an M1 Pro Max with 64GB of RAM is the best option!"
].joined()
newLaptop.isFavorite = true
newLaptop.lastModified = Date()
try container.viewContext.save()
}
}
5. Action.swift
import UIKit
// 1
enum ActionType: String {
case newNote = "NewNote"
case editNote = "EditNote"
}
// 2
enum Action: Equatable {
case newNote
case editNote(identifier: String)
// 3
init?(shortcutItem: UIApplicationShortcutItem) {
// 4
guard let type = ActionType(rawValue: shortcutItem.type) else {
return nil
}
// 5
switch type {
case .newNote:
self = .newNote
case .editNote:
if let identifier = shortcutItem.userInfo?["NoteID"] as? String {
self = .editNote(identifier: identifier)
} else {
return nil
}
}
}
}
// 6
class ActionService: ObservableObject {
static let shared = ActionService()
// 7
@Published var action: Action?
}
6. EditNote.swift
import SwiftUI
// MARK: Edit Note
struct EditNote: View {
// MARK: Editor
private enum Editor: Hashable {
case title
case body
}
// MARK: Properties
/// The text input that is currently focused
@FocusState private var focusedEditor: Editor?
/// The note being edited
@ObservedObject var note: Note
/// The store of all notes used for saving changes
@EnvironmentObject var noteStore: NoteStore
// MARK: Body
var body: some View {
VStack(alignment: .leading) {
HStack {
TextField("Title", text: $note.wrappedTitle)
.padding(.horizontal, 6)
.font(.system(size: 18, weight: .bold, design: .default))
.frame(minHeight: 35, maxHeight: 35)
.border(Color.accentColor, width: focusedEditor == .title || note.wrappedTitle.isEmpty ? 1 : 0)
.focused($focusedEditor, equals: .title)
Button(action: toggleFavorite) {
Image(systemName: note.isFavorite ? "star.fill" : "star")
.foregroundColor(note.isFavorite ? .yellow : .gray)
}
}
TextEditor(text: $note.wrappedBody)
.font(.system(size: 14, weight: .regular, design: .default))
.border(Color.accentColor, width: focusedEditor == .body || note.wrappedBody.isEmpty ? 1 : 0)
.focused($focusedEditor, equals: .body)
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Edit Note")
.padding(8.0)
.onDisappear(perform: onDisappear)
}
// MARK: Actions
func toggleFavorite() {
note.isFavorite.toggle()
}
func onDisappear() {
noteStore.saveIfNeeded()
}
}
// MARK: Previews
struct EditNote_Previews: PreviewProvider {
static let noteStore = NoteStore.preview()
static var notes: [Note] {
(try? noteStore.container.viewContext.fetch(Note.fetchRequest())) ?? []
}
static var previews: some View {
EditNote(note: notes[0])
.previewLayout(.sizeThatFits)
.environmentObject(noteStore)
}
}
7. NoteList.swift
import CoreData
import SwiftUI
// MARK: Note List
struct NoteList: View {
// MARK: Properties
/// The Core Data query used to populate the notes presented within the list
@FetchRequest(
entity: Note.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Note.lastModified, ascending: false)
]
) var notes: FetchedResults<Note>
/// The currently selected note (binded to the navigation of the `EditNote` view)
@State private var selectedNote: Note?
/// The class used for managing the stored notes
@EnvironmentObject var noteStore: NoteStore
@EnvironmentObject var actionService: ActionService
@Environment(\.scenePhase) var scenePhase
// MARK: Body
var body: some View {
NavigationView {
ZStack {
// MARK: Navigation
NavigationLink(value: $selectedNote) { note in
EditNote(note: note)
}
// MARK: Content
List {
ForEach(notes) { note in
Button(
action: { selectedNote = note },
label: { NoteRow(note: note) }
)
}
.onDelete(perform: deleteNotes(atIndexSet:))
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Notes")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: createNewNote) {
Image(systemName: "square.and.pencil")
}
}
}
// 1
.onChange(of: scenePhase) { newValue in
// 2
switch newValue {
case .active:
performActionIfNeeded()
case .background:
updateShortcutItems()
// 3
default:
break
}
}
}
}
}
// MARK: Actions
/// Deletes each note and commits the change
func deleteNotes(atIndexSet indexSet: IndexSet) {
for index in indexSet {
noteStore.container.viewContext.delete(notes[index])
}
noteStore.saveIfNeeded()
}
/// Creates a new note and selects it immediately
func createNewNote() {
selectedNote = noteStore.createNewNote()
}
func performActionIfNeeded() {
// 1
guard let action = actionService.action else { return }
// 2
switch action {
case .newNote:
createNewNote()
case .editNote(let identifier):
selectedNote = noteStore.findNote(withIdentifier: identifier)
}
// 3
actionService.action = nil
}
func updateShortcutItems() {
UIApplication.shared.shortcutItems = notes.compactMap(\.shortcutItem)
}
}
// MARK: Previews
struct NoteList_Previews: PreviewProvider {
static let noteStore = NoteStore.preview()
static var previews: some View {
NoteList()
.environmentObject(noteStore)
.environment(\.managedObjectContext, noteStore.container.viewContext)
}
}
8. NoteRow.swift
import SwiftUI
// MARK: Note Row
struct NoteRow: View {
// MARK: Properties
@ObservedObject var note: Note
// MARK: Body
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 2.0) {
if !note.wrappedTitle.isEmpty {
Text(note.wrappedTitle)
.font(.system(size: 15, weight: .bold, design: .default))
.multilineTextAlignment(.leading)
.lineLimit(1)
}
Text(note.wrappedBody)
.font(.system(size: 14, weight: .regular, design: .default))
.multilineTextAlignment(.leading)
.lineLimit(3)
}
Spacer()
Label("Toggle Favorite", systemImage: note.isFavorite ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(note.isFavorite ? .yellow : .gray)
}
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
.foregroundColor(Color(.label))
}
}
// MARK: Previews
struct NotesRow_Previews: PreviewProvider {
static let noteStore = NoteStore.preview()
static var notes: [Note] {
(try? noteStore.container.viewContext.fetch(Note.fetchRequest())) ?? []
}
static var previews: some View {
NoteRow(note: notes[2])
.previewLayout(.fixed(width: 300, height: 70))
}
}
9. NavigationLink+Value.swift
extension NavigationLink where Label == EmptyView {
/// Convenience initializer used to bind a `NavigationLink` to an optional value.
///
/// When a non-nil value is assigned, the link will be triggered. Setting the value to `nil` will pop the navigation link again.
///
/// ```swift
/// struct MyListView: View {
/// @EnvironmentObject var modelStore: ModelStore
/// @State private var selectedModel: MyModel?
///
/// var body: someView {
/// NavigationView {
/// ZStack {
/// // Navigation
/// NavigationLink(value: $selectedModel) { model in
/// MyDetailView(model: model)
/// }
///
/// // List Content
/// ForEach(modelStore) { model in
/// Button(action: { selectedModel = model }) {
/// MyRow(model: model)
/// }
/// }
/// }
/// }
/// }
/// }
/// ```
init<Value, D: View>(
value: Binding<Value?>,
@ViewBuilder destination: @escaping (Value) -> D
) where Destination == D? {
// Create wrapping arguments that erase the value from the destination and binding
let destination = value.wrappedValue.map { destination($0) }
let isActive = Binding<Bool>(
get: { value.wrappedValue != nil },
set: { newValue in
if newValue {
assertionFailure("Programatically setting isActive to `true` is not supported and will be ignored")
}
value.wrappedValue = nil
}
)
// Invoke the original initializer with the mapped destination and correct binding
self.init(destination: destination, isActive: isActive) {
EmptyView()
}
}
}
后记
本篇主要讲述了
Home Screen Quick Actions for SwiftUI App
,感兴趣的给个赞或者关注~~~
网友评论