美文网首页Swift
3D Touch (二) —— Home Screen Quic

3D Touch (二) —— Home Screen Quic

作者: 刀客传奇 | 来源:发表于2022-04-06 20:51 被阅读0次

    版本记录

    版本号 时间
    V1.0 2022.04.05 星期二 清明节

    前言

    3D TouchiPhone 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,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:3D Touch (二) —— Home Screen Quic

        本文链接:https://www.haomeiwen.com/subject/voylsrtx.html