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 中文版
如何在 SwiftUI App 中開發 Live Activities
SwiftUI 框架

如何在 SwiftUI App 中開發 Live Activities

Live Activities 首次於 iOS 16 推出,是 Apple 最令人興奮的更新之一,能讓 App 與使用者在即時互動上更有連結。它不再需要使用者不斷打開 App,Live Activities 可以讓資訊直接顯示在鎖定畫面和 Dynamic Island 上。
使用 Tool Calling 強化 Foundation Models 功能
AI

使用 Tool Calling 強化 Foundation Models 功能

在前幾篇教學中,我們介紹了 Foundation Models 在 iOS 26 中的運作方式,以及如何使用這個全新框架打造具備 AI 功能的應用。我們也介紹了 @Generable 巨集,它能輕鬆地將模型回應轉換為結構化的 Swift 類型。 現在,在這個 Foundation
活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App
AI

活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App

在前一篇教學中,我們介紹了 Foundation Models 框架,並示範了如何用它來進行基本的內容生成。那個過程相當簡單——你提供一個提示詞(prompt),等幾秒鐘,就能獲得自然語言的回應。在我們的範例中,我們建立了一個簡單的問答 App,讓使用者可以提問,App 則直接顯示生成的文字。 但如果回應變得更複雜——你需要把非結構化文字轉換為結構化的物件呢? 舉例來說,
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。