#20 導入Xcode (芝加哥藝術博物館)
從第一單元到目前為止,我們所有程式都是在 Swift Playgrounds 裡面執行,不過,若想開發跨產品(Mac, iPhone, iPad, Apple Watch, Apple TV)的程式,則必須使用 Xcode,這也是Apple陣營的程式設計師唯一選擇,那麼在 Swift Playgrounds 開發的程式,能不能用在 Xcode 呢?
答案是肯定的,本節我們將範例程式 3-5c 導入 Xcode 執行,看看有何差別。
Xcode 與 Swift Playgrounds 兩者的使用環境有所不同,下表是兩者的簡單比較。
表3-5 Xcode 與 Swift Playgrounds 差異
| # |
Xcode |
Swift Playgrounds |
| 作業系統 |
只能在MacOS執行 |
MacOS 或 iPadOS |
| 硬體設備 |
Mac (iMac, Macbook, Mac mini) |
Mac (iMac, Macbook, Mac mini), iPad |
| 語言版本 |
只有英文版 |
支援14種語言(繁中、簡中、英、法、瑞典、義、荷、葡、西、土、德、日、韓、泰) |
| 大小 |
12.4GB (v13.1) |
232MB (v3.4.1) |
| 產出App |
所有Apple產品 |
僅iOS |
| 費用 |
使用免費 上傳App Store須繳年費 |
使用免費 上傳App Store須繳年費 |
| 發布年代 |
2003年(v1.0) |
2016年(v1.0) |
所以需要一台Mac才能使用Xcode,本節範例筆者用Macbook Air (M1),花了將近12小時才升級完 MacOS 12.0.1 與 Xcode 13.1。
安裝好 Xcode 之後,依照以下步驟執行:
(一)執行 Xcode,選擇開啟新專案 "Create a new Xcode project":

(二)選擇 iOS App 或 Multiplatform (跨產品)

(三)輸入App名稱,選擇 SwiftUI 介面。注意App名稱只能用英文:

(四)選擇檔案存放目錄

(五)顯示 Hello World! 範例程式。

這是Xcode給每個新專案的範例程式,顯示兩個 struct 型態宣告,一個名稱 ContentView 是預設的 App 進入點,也就是程式開始執行的視圖;另一個名稱 ContentView_Previews,所屬規範是 PreviewProvider 而不是 View,只要是符合 PreviewProvider 規範,都可以在上圖程式碼右側欄位中「預覽」執行結果。
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
我們先按上圖預覽欄位的 "Resume" 按鈕來確認能否順利執行。
(六)按一下 "Resume" 開始預覽,正常結果如下圖

(七)更換為「芝加哥藝術博物館」程式
原來的 Hello world! 範例程式不要動,將範例 3-5c 程式碼完整拷貝到上方,將第一行與最後一行 "Remark" 起來(或刪除):
// import PlaygroundSupport
import SwiftUI
...
// PlaygroundPage.current.setLiveView(芝加哥藝術博物館())
原來我們在 Swift Playgrounds 寫的程式,第一行與最後一行只在 Swift Playgrounds 裡面才用得到,換到 Xcode 就不需要了。
然後更改 ContentView 主體,改成「芝加哥藝術博物館()」:
struct ContentView: View {
var body: some View {
芝加哥藝術博物館()
}
}
(八)執行預覽

以上執行過程的影片如下:
在 Xcode 上面的完整程式碼如下:
//
// ContentView.swift
// Shared
//
// Created by Heman Lu on 2021/10/29.
//
// 3-5c 整合(芝加哥藝術博物館)
// Created by Heman, 2021/10/13
// import PlaygroundSupport
import SwiftUI
// Refer to https://api.artic.edu/api/v1/artworks/search?q=van+gogh&limit=100
struct 搜尋結果: Codable {
let pagination: 分頁資訊
let data: [搜尋品項]
}
struct 分頁資訊: Codable {
let total: Int
let limit: Int
let offset: Int
let total_pages: Int
let current_page: Int
}
struct 搜尋品項: Codable, Identifiable {
let api_link: URL?
let id: Int
let title: String
}
// Refer to https://api.artic.edu/api/v1/artworks/28560
struct 藝術作品: Codable {
let data: 作品資訊
let config: 配置資訊
}
struct 作品資訊: Codable, Identifiable {
let id: Int
let api_link: URL?
let title: String
let date_display: String
let artist_display: String
let artist_id: Int
let artist_title: String
let image_id: String
}
struct 配置資訊: Codable {
let iiif_url: String
let website_url: String
}
// 顯示「作品圖」陣列
struct 作品圖: Identifiable {
var id: Int = 0
var 作品名稱: String = ""
var 作者: String = ""
var image_id: String = ""
var 圖檔網址: URL?
}
struct 抓圖: View {
var myURL: URL
@State var 下載圖片: UIImage?
init(_ p: URL) {myURL = p}
func 下載() {
URLSession.shared.dataTask(with: myURL) { 回傳資料, 回應碼, 錯誤碼 in
if let 圖檔 = UIImage(data: 回傳資料!) {
print(回應碼 ?? "No response")
下載圖片 = 圖檔
} else {
print(錯誤碼 ?? "No error")
}
}.resume()
}
var body: some View {
if 下載圖片 == nil {
ProgressView()
.onAppear {
下載()
}
} else {
Image(uiImage: 下載圖片!)
.resizable()
.scaledToFit()
}
}
}
struct 抓大圖: View {
@State var 下載圖片: UIImage?
@State var 縮放: Bool = false
@State var 位移: CGSize = .zero
@State var 上次位移: CGSize = .zero
var 圖片編號: String
init(_ p: String) {圖片編號 = p}
func 下載() {
let 網址 = "https://www.artic.edu/iiif/2/" + 圖片編號 + "/full/max/0/default.jpg"
guard let myURL = URL(string: 網址) else { return }
URLSession.shared.downloadTask(with: myURL) { 暫存檔, 回應碼, 錯誤碼 in
do {
let 回傳資料 = try Data(contentsOf: 暫存檔!)
if let 圖檔 = UIImage(data: 回傳資料) {
print(回應碼 ?? "No response")
下載圖片 = 圖檔
} else {
print(錯誤碼 ?? "No error")
}
} catch { print("Data I/O error") }
}.resume()
}
var 拖曳: some Gesture {
DragGesture(minimumDistance: 0.0, coordinateSpace: .local)
.onChanged { 拖曳參數 in
print("[onChanged]目前位置:", 拖曳參數.location)
// print("[onChanged]目前位移 上次位移", 拖曳參數.translation, 上次位移)
位移.height = 上次位移.height + 拖曳參數.translation.height
位移.width = 上次位移.width + 拖曳參數.translation.width
}
.onEnded { 拖曳參數 in
print("[onEnded]位移:", 拖曳參數.translation)
if abs(拖曳參數.translation.height) < 3 && abs(拖曳參數.translation.width) < 3 {
縮放.toggle()
位移 = CGSize.zero
上次位移 = CGSize.zero
}
上次位移.height += 拖曳參數.translation.height
上次位移.width += 拖曳參數.translation.width
}
}
var body: some View {
if 下載圖片 == nil {
ProgressView()
.onAppear {
下載()
}
} else {
Image(uiImage: 下載圖片!)
.resizable()
.aspectRatio(contentMode: .fit)
.scaleEffect(縮放 ? 5.0 : 1.0)
.offset(位移)
.gesture(拖曳)
}
}
}
struct 芝加哥藝術博物館: View {
@State var inputText = "Van Gogh"
@State var 搜尋字串 = "van gogh"
@State var 作品列表: [搜尋品項]?
@State var 作品圖集: [作品圖] = []
// let 網址 = "https://api.artic.edu/api/v1/artworks/search?q=van+gogh&limit=100"
// guard let myURL = URL(string: 網址) else { return }
func 更新作品列表(_ 字串參數: String) {
var myURLComponent = URLComponents()
myURLComponent.scheme = "https"
myURLComponent.host = "api.artic.edu"
myURLComponent.path = "/api/v1/artworks/search"
myURLComponent.query = "q=\(字串參數)&limit=8"
guard let myURL = myURLComponent.url else { return }
URLSession.shared.dataTask(with: myURL) { 回傳資料, 回傳碼 , 錯誤碼 in
if let 解碼資料 = 回傳資料 {
do {
let 解碼結果 = try JSONDecoder().decode(搜尋結果.self, from: 解碼資料)
print(回傳碼 ?? "No response")
作品列表 = 解碼結果.data
for 作品 in 作品列表! {
取得作品資訊(作品.api_link!)
}
} catch {
print("JSON解碼錯誤")
}
} else {
print(錯誤碼 ?? "No error")
}
}.resume()
}
// let 網址 = "https://api.artic.edu/api/v1/artworks/28560"
// guard let myURL = URL(string: 網址) else { return }
// 圖檔網址 IIIF v2 standard format:
// https://www.artic.edu/iiif/2/25c31d8d-21a4-9ea1-1d73-6a2eca4dda7e/full/843,/0/default.jpg
func 取得作品資訊(_ myURL: URL) {
var 單項作品: 作品資訊?
var 目前作品 = 作品圖()
var myURLComponent = URLComponents()
URLSession.shared.dataTask(with: myURL) { 回傳資料, 回傳碼 , 錯誤碼 in
if let 待解碼資料 = 回傳資料 {
do {
let 尺寸 = "600,"
let 解碼結果 = try JSONDecoder().decode(藝術作品.self, from: 待解碼資料)
print(回傳碼 ?? "No response")
單項作品 = 解碼結果.data
目前作品.id = 單項作品!.id
目前作品.作品名稱 = 單項作品!.title
目前作品.作者 = 單項作品!.artist_display
目前作品.image_id = 單項作品!.image_id
myURLComponent.scheme = "https"
myURLComponent.host = "www.artic.edu"
myURLComponent.path = "/iiif/2/\(單項作品!.image_id)/full/\(尺寸)/0/default.jpg"
myURLComponent.query = ""
目前作品.圖檔網址 = myURLComponent.url
作品圖集 = 作品圖集 + [目前作品]
} catch {
print("JSON解碼錯誤")
}
} else {
print(錯誤碼 ?? "No error")
}
}.resume()
}
let 單欄 = [GridItem(.flexible()), GridItem(.flexible())]
var body: some View {
VStack(spacing: 0) {
Text("Art Institute of Chicago")
.font(.title)
.bold()
ZStack {
Rectangle()
.foregroundColor(.orange)
.frame(height: 50)
HStack {
Image(systemName: "magnifyingglass")
.font(.title2)
TextField("Search for artworks", text: $inputText, onCommit: {
搜尋字串 = inputText
作品列表 = nil
作品圖集 = []
})
.font(.title2)
.foregroundColor(.black)
.background(Color.white)
}
.padding()
}
if 作品列表 == nil {
ProgressView()
.onAppear {
更新作品列表(搜尋字串)
}
} else {
NavigationView {
ScrollView {
LazyVGrid(columns: 單欄) {
ForEach(作品圖集) { 作品 in
NavigationLink(destination: 抓大圖(作品.image_id)) {
VStack {
抓圖(作品.圖檔網址!)
HStack {
Text(作品.作品名稱)
.font(.body)
.bold()
Spacer()
Text(作品.作者)
.font(.callout)
}
}
}
}
}
}
.navigationTitle(搜尋字串)
Text("⬅︎左方導覽頁面選擇作品")
}
}
}
}
}
// PlaygroundPage.current.setLiveView(芝加哥藝術博物館())
// import SwiftUI
struct ContentView: View {
var body: some View {
芝加哥藝術博物館()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
註解
- 本課程並不推薦初學者用Xcode學習Swift/SwiftUI程式設計,希望還是推廣 Swift Playgrounds,不管是作為初學者的主要環境,或是程式設計師的測試環境,Swift Playgrounds都能勝任愉快。
- 所以本節內容,主要是讓讀者不必擔心程式碼無法轉到Xcode,後面課程仍會繼續以Swift Playgrounds作為主要開發環境。
- 為什麼在Xcode上面執行程式稱為「預覽」呢?因為未來產出的App可以在不同產品中執行,即使指定 iOS 平台,在不同型號的 iPhone 也會有不同解析度的情況,在Xcode上面可以指定iOS版本與iPhone型號,然後在該特定產品的模擬器中「預覽」執行結果,對調整UI畫面(視圖大小、位置、動態效果等)很有幫助。
- 所以Xcode最大的好處就是有完整Apple產品的「模擬器」(Simulator),這是Xcode大小超過12GB的原因,也是與Swift Playgrounds主要差異之一(Swift Playgrounds只含iOS模擬器)。