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 中文版
iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場
SwiftUI 框架

iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場

Apple 的工程師可能早已認識到,許多 iOS 開發者都希望能夠重現 App Store 應用程式中的優雅 Hero 動畫。由於從頭實現這種動畫通常需要耗費大量時間與精力,Apple 在 iOS 18 SDK 中納入了這項功能。 透過這次更新,你現在只需少量的程式碼就能在自己的應用程式中實現類似的動畫過渡效果。這項重大改進讓開發者能夠創造出更具視覺吸引力且流暢的過渡效果,
如何使用 Vision APIs 從圖像中辨識文字
AI

如何使用 Vision APIs 從圖像中辨識文字

Vision 框架長期以來一直包含文字識別功能。我們已經有詳細的教程,向你展示如何使用 Vision 框架掃描圖像並執行文字識別。之前,我們使用了 VNImageRequestHandler 和 VNRecognizeTextRequest 來從圖像中提取文字。 多年來,Vision 框架已經顯著演變。在 iOS 18 中,Vision
iOS 18更新:SwiftUI 新功能介紹
SwiftUI 框架

iOS 18更新:SwiftUI 新功能介紹

SwiftUI的技術不斷演進,每次更新都讓 iOS 應用程式開發變得更加便捷。隨著 iOS 18 Beta 的推出,SwiftUI 引入了多個令人興奮的新功能,使開發者僅需幾行程式碼即可實現出色的效果。 本教學文章旨在探索這個版本中的幾項主要改進,幫助你了解如何運用這些新功能。 浮動標籤列 (Floating Tab Bar)SwiftUI中的標籤視圖(Tab
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。