SwiftUI 框架

利用 SwiftUI 在 iOS 15 中構建一個簡單的繪畫 App

在開發 App 時,有一件事情是開發者一定要做的,就是繪製一些簡單圖形。我們可以利用 Paint 或 Preview 繪製這些簡單的圖形,這兩個都是很好的 App,但有時還是會有點不足。在這篇文章中,Mark 會帶大家利用 SwiftUI,在 iOS 15 中構建一個簡單的繪畫 App,來解決這個問題。
利用 SwiftUI 在 iOS 15 中構建一個簡單的繪畫 App
利用 SwiftUI 在 iOS 15 中構建一個簡單的繪畫 App
In: SwiftUI 框架

本篇原文(標題:Build a Painting App in iOS 15 With SwiftUI)刊登於作者 Medium,由 Mark Lucking 所著,並授權翻譯及轉載。

在開發 App 時,有一件事情是開發者一定要做的,就是繪製一些簡單圖形。我們可以利用 Paint 或 Preview 繪製這些簡單的圖形,這兩個都是很好的 App,但有時還是會有點不足。因為當我們要建立一個圖形的點陣圖 (Bitmap) 時,總會有想要的 iOS 顏色或尺寸。雖然我們都可以在 Paint/Preview 中設定這兩個屬性,但有時卻無法符合完美像素 (pixel-perfect)。

在這篇文章中,我會帶大家構建一個簡單的繪畫 App,來解決這個問題。在文章的結尾,大家也可以下載完整的 App。

簡介

我想構建一個 iOS App 來繪製一些簡單的圖形。這個 App 的操作應該像 Paint 或 Preview App 那樣直觀,我們可以以不同顏色創建、刪除、調整尺寸、複製和貼上、以及任意擺放不同圖形,也可以構建特定尺寸的圖形,圖形可以是線條,也可以是不同的形狀,至少要有三角形、正方形和圓形。當然,我們也需要儲存圖形,並簡單匯出到開發平台的功能。與 Preview 和 Paint App 不同的是,我想 App 可以有圖層的概念。如果 App 可以讓我們添加文本到圖形、以及繪製線條就更好了。

編寫程式碼

我最初是想構建一個混合 UIKit/SwiftUI 的 App,因為 SwiftUI 中沒有觸摸手勢 (touch gesture)。幸好,我發現 SwiftUI 中可以使用最小距離為 0 的拖動手勢,這樣其操作就會與 UIKit 上的觸摸手勢相同。因此,最後我利用了純 SwiftUI 構建這個 App。

第一個版本最複雜的地方,就是創建 (creating) 與選擇 (selecting) 圖形背後的邏輯,這個步驟需要反複試驗才能解決。我從 ObservableObject 開始,並利用它把 Canvas 視圖和 ContentView 的數據共享。

class Cords: ObservableObject {
  @Published var cord:[CGPoint] = [CGPoint](repeating: CGPoint.zero, count: 128)
  @Published var size:[CGSize] = [CGSize](repeating: CGSize.zero, count: 128)
  @Published var selected:[Bool] = [Bool](repeating: false, count: 128)
  static var shared = Cords()
}

我發現利用 1 個結構會比用 3 個陣列 (array) 更好,不過我在構建原型 (prototype) 的時候添加了所謂元素,我很快就會重構這個部分。

接著,我們要設置 CanvasView,這個視圖會在 Canvas 的圖層中構建不同圖形。文末的範例就用了橢圓形,它比 rect 好用,我會再花時間改善這個功能。

struct LeCanvas: View {
  @ObservedObject var cords = Cords.shared
  @Binding var indx:Int
  var body: some View {
    Canvas(opaque: false, colorMode: .nonLinear, rendersAsynchronously: true, renderer: { context, size in
      for i in 0..<cords.cord.count {
        context.drawLayer { layerContext in
          layerContext.withCGContext { cgContext in
            if cords.cord[i] != CGPoint.zero {
              cgContext.move(to: cords.cord[i])
              let rect = CGRect(origin: cords.cord[i], size: cords.size[i])
              let path = CGPath(rect: rect, transform: nil)
//              let path = CGPath(ellipseIn: rect, transform: nil)
              cgContext.addPath(path)
              cgContext.setStrokeColor(cords.selected[i] ? UIColor.red.cgColor: UIColor.blue.cgColor)
              cgContext.setFillColor(UIColor.clear.cgColor)
              cgContext.setLineWidth(2)
              cgContext.drawPath(using: .eoFillStroke)
            }
          }
        }
      }
    })
  }
}

在這裡,我們希望使用這篇文章所介紹的方法,以 X edges 的路徑 (path) 來建構一個圖形。

接下來,在 ContentView 中,我們就可以用這個邏輯,來設置選擇/取消 選擇圖像的操作。

struct ContentView: View {
  @GestureState var foo = CGPoint.zero
  @ObservedObject var cords = Cords.shared
  @State var indx = 0
  @State var selected = false
  @State var newShape = true
  @State var selectedIndx = 0
  var body: some View {
    
    LeCanvas(indx: $indx)
      .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
                .updating($foo) { value, state, transaction in
        if newShape {
          let nudge = CGSize(width: CGFloat(cords.size[indx].width), height: CGFloat(cords.size[indx].height))
          let newPoint = CGPoint(x: value.location.x - (nudge.width/2), y: value.location.y - (nudge.height/2))
          cords.cord[indx] = newPoint
          if value.translation.width > 4 || value.translation.height > 4 {
            cords.size[indx] = CGSize(width: value.translation.width, height: value.translation.height)
          }
        }
        if selected {
          let shape = CGRect(origin: cords.cord[selectedIndx], size: cords.size[selectedIndx])
          if shape.contains(value.location) {
            let nudge = CGSize(width: CGFloat(cords.size[selectedIndx].width), height: CGFloat(cords.size[selectedIndx].height))
            let newPoint = CGPoint(x: value.location.x - (nudge.width/2), y: value.location.y - (nudge.height/2))
            cords.cord[selectedIndx] = newPoint
          }
        }
      }.onEnded({ value in
        newShape = false
        selected = true
        if value.translation.width < 4 && value.translation.height < 4 {
          if searchin(value: value, cords: cords) {
            DispatchQueue.main.async {
              selected = true
              newShape = false
            }
          } else {
            DispatchQueue.main.async {
              deselect(cords: cords)
              selected = false
              newShape = true
              indx += 1
            }
          }
        }
      })
      )
    HStack {
      Text("Selected \(selected.description) \(indx)")
      Text("New Shape \(newShape.description)")
        .onTapGesture {
          DispatchQueue.main.async {
            indx += 1
          }
        }
    }
  }

在以上的程式碼中,我在除錯 (debug) 的時候用了兩個變數,來為我們追蹤一些重要的變數。另外,你也可以注意到我在幾個地方用了 DispatchQueue,這讓我們可以繞過紫色警告,並確保 UI 僅在主執行緒 (main thread) 上更新。

最後,我們需要構建 2 個 helper routine,來選擇/取消選擇螢幕上的物件:

func deselect(cords:Cords) {
    for i in 0..<cords.cord.count {
      if cords.cord[i] != CGPoint.zero {
        DispatchQueue.main.async {
          cords.selected[i] = false
        }
      }
    }
  }
  
  func searchin(value: GestureStateGesture<DragGesture, CGPoint>.Value, cords:Cords) -> Bool {
    for searchin in 0..<cords.cord.count {
      let shape = CGRect(origin: cords.cord[searchin], size: cords.size[searchin])
      if shape.contains(value.location) {
        selectedIndx = searchin
        cords.selected[searchin] = true
        return true
      }
    }
    return false
  }

現在,讓我們整合所有程式碼,並在模擬器上運行,就可以試用範例 App 了!

painting-app-swiftui

從以上的 GIF 可見,我畫了一個圓形,只要點擊圓形邊框內的位置,就可以選擇圖形,並將其移動到其他圓形旁邊。然後我點擊其他位置,就可以取消選擇它,並創建另一個圖形。

我還沒有試過用這個 App 繪製客製化圖形,讓我們在下一篇文章再深入探討。謝謝你的閱讀。

本篇原文(標題:Build a Painting App in iOS 15 With SwiftUI)刊登於作者 Medium,由 Mark Lucking 所著,並授權翻譯及轉載。

作者簡介:Mark Lucking,編程資歷超過 35 年,熱愛使用及學習 Swift/iOS 開發,定期在 Better ProgrammingThe StartUpMac O’ClockLevel Up Coding、及其它平台上發表文章。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。

作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
更多來自 AppCoda 中文版
如何使用 Swift 整合 Google Gemini AI
SwiftUI 框架

如何使用 Swift 整合 Google Gemini AI

在即將到來的 WWDC,Apple 預計將會發佈一個本地端的大型語言模型 (LLM)。 接下來的 iOS SDK 版本將讓開發者更輕易地整合 AI 功能至他們的應用程式中。然而,當我們正在等待 Apple 推出自家的生成 AI 模型時,其他公司(如 OpenAI
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。