利用 LibraryContentProvider 重用 SwiftUI 視圖 大大加速開發過程!


在 WWDC 2020 中,Apple 為生態系統的整個開發過程帶來了許多新功能和改善,肯定每個開發者都能從中找到覺得興奮的新功能。部分新功能就是關於 Xcode 12 及 SwiftUI 的,我們在先前的文章已經簡單地介紹過 Xcode 12 及 SwiftUI 的新功能。開發者可以利用新的 LibraryContentProvider 功能,創造出可重用的 SwiftUI 視圖與修飾器,並且把它們加入到 Xcode 的函式庫當中。

你可能會問:這個功能有重要到要寫一篇新的教學文章來討論嗎?沒錯,這功能不僅為 SwiftUI 視圖提供存為函式庫元件、或是在開發過程讓我們輕鬆地重用程式碼。更重要的是,這功能以隨插即用 (plug-and-play) 的方式,讓開發者在不同專案中重用 SwiftUI 視圖與修飾器,甚至是分享給其他開發者。這讓我們可以將客製化的項目儲存在函式庫當中,需要的時候就像是預設的元件一樣,直接拖拉到實作之中。很明顯,這個功能為整個開發過程大大增值。

因此,在本次的教學文章中,我們詳細看看如何透過 Xcode 函式庫,讓我們可存取及重用 SwiftUI 視圖與修飾器。不過這只是我們將要討論的主題之一,另外,我們也會進一步研究,如何透過 Swift Package 在最初定義的專案之外的地方重用這些項目。為了達到完全的可重用性,在過程之中我們會遇到一些特殊情況,需要做一些額外的步驟,才能達到我們的目的。不過,你看到最後的成果後,相信你會覺得一切步驟都是值得的!

所以,讓我們一起探索這個可以加速開發過程的新功能吧!

概覽

作為本篇教學文章的範例,我們會以一個名為 ReusableViewsAndModifiers 的簡單專案開始,你可以從這裡下載專案。

這個專案之中最重要的元素就是簡易客製進度視圖的實作,名為 MyProgressView,我們會把實作用來做「實驗」的目標視圖。隨著文章的介紹,我們將會加入新程式碼來豐富原來的實作。之後,我們甚至會將起始專案中的檔案移動到 Swift Package。

在下一個部分,我們首先要將 MyProgressView 放到另一個 SwiftUI 視圖 ContentView 中。在這當中,你會發現起始專案只有兩個按鈕的實作,一個按鈕用來更新進度,並顯示於進度視圖,另一個就用來重設進度。這樣簡單的功能已經足夠於實現我們的目標。

之後,我們會開始探索前文所說的新事物。首先,我們會學習如何創造客製函式庫項目,讓我們可以在專案裡面輕易地重用 MyProgressView 視圖。在 Xcode 函式庫中創建自己的客製化項目,本身就已經非常有趣了,但是僅僅可以在同一個專案中重用視圖是不夠的,因此我們將會跨越到其他專案,並使用 Swift Package 來將 MyProgressView 移動到別的專案,讓客製化視圖在新專案都可以重用!這個過程會有許多有趣的地方,並且需要進行一些特定的步驟,我們會隨著教學文章一步步說明。

除了讓客製化 SwiftUI 視圖可重用之外,本篇文章還有另一個目標,就是學習如何讓修飾器都可以重用。我們將會創建自己的客製化修飾器,並封裝其他預設的修飾器,以改變按鈕的外觀。我們也會學到如何將客製化修飾器加入為函式庫項目,並在多個專案中使用。我們將會透過 Swift Package 來傳送所有東西,讓我們可以在起始專案之外的地方使用原本的內容。同樣地,我們也會在修飾器的部分遇到一些有趣的細節。

簡單介紹過我們要學習的東西之後,你可以花點時間看看起始專案的內容,一旦你準備好了,就跟著接下來的文章繼續探索吧!

客製化進度視圖

我們想實現可重用性的 SwiftUI 視圖,就是 MyProgressView.swift 檔案內的 MyProgressView 客製化進度視圖。我們會以這個視圖為基礎,來定義幾個客製化函式庫項目。實際上,進度視圖只不過是兩個 RoundedRectangle 物件,當中用了一些修飾器,來讓它顯示為我們想要的樣子,其中一個修飾器是 ZStack,用來將其中一個視圖疊在另一個之上。

第二個長方形(在上層的長方形)的顏色會與下層長方形的不同,以顯示當前的進度,上層長方形的寬度是依據 progress 屬性 (property) 來指定,來顯示目前任務的進度。實際上,它的寬度是經由 getProgress() 方法所回傳的數值決定,而該方法就是依據 progress 屬性的值來回傳。這簡單的方法會確保寬度數值是在 [0, 100] 的範圍之內,如果得到的數值是在範圍之外,就會按情況回傳 0 或 100。你可以從 Preview 中查看進度視圖的實際範例。

在我們開始動手做之前,讓我們先看看這個視圖是如何使用的。切換到 ContentView.swift 檔案,並找到下列註解的位置:

// Use the progress view here

將它替換成以下這行程式碼,來初始化 MyProgressView 實例:

MyProgressView(progress: progress, progressColor: .orange)

這行程式碼將 MyProgressView 視圖放置在兩個按鈕之間,傳遞 progress 屬性,並以橘色來顯示目前的進度。

現在來試用一下,到右方的 Canvas 視窗,在 iPhone 預覽上方的工具列點擊 Play 按鈕,來切換到 Live Preview 模式。接著,使用上方的按鈕來增加進度,並使用下方的按鈕來重設進度,如此一來,你就會看到進度視圖隨著你的動作而更新。

swiftui-progress-bar-view-modifier

我們剛剛簡單介紹的進度視圖,就是我們本次的「實驗對象」。現在我們已經看過它原本的行為,讓我們開始研究新的東西,創造一個函式庫的項目,讓我們輕易地在任何專案中使用進度視圖。

備註:在範例專案當中重用客製化進度視圖,看起來可能沒有什麼意義,但是日後我們遇到更大的專案時,可以重用視圖就相當有用!

將進度視圖加入到函式庫

要在 Xcode 函式庫建立一個新項目,第一步就是要定義一個新的客製化型別,並遵循 LibraryContentProvider 協定。你可以任意為客製化型別命名,不過最好是命名為能夠描述該型別的名稱。

MyProgressView.swift 檔案中,在所有內容的下方加入下列程式碼:

struct MyProgressViewLibraryContent: LibraryContentProvider {

}

以上的協定有兩個必要條件:

  • 一個 views 屬性,用來加入新的視圖項目到 Xcode 函式庫當中。
  • 一個 modifiers(base:) 函式,用來加入新的修飾器項目到 Xcode 函式庫當中。稍後我們會討論這一部分。

上述兩個條件的共通點,是兩種情況都必須回傳 LibraryItem 物件的集合。以客製化視圖的情況來說,我們必須以一個新的 SwiftUI 視圖實例來初始化這類物件。如果同一個視圖有多種變形,需要對應到函式庫中的多個項目,那麼就需要創建多個 LibraryItem 物件(我們將會在下個部分看看這種情況)。

讓我們在這定義 views 屬性。請注意,@LibraryContentBuilder 特性 (attribute) 應該要放在屬性定義之前:

struct MyProgressViewLibraryContent: LibraryContentProvider {
    @LibraryContentBuilder
    var views: [LibraryItem] {

    }
}

我們必須在上面 getter 方法的主體中,將 SwiftUI 視圖實例給予新的  LibraryItem  物件,讓它們可以在 Xcode 函式庫中使用。

關於 @LibraryContentBuilder 特性的備註:

@LibraryContentBuilder 讓我們不需要從 views 的 getter 方法回傳一個 LibraryItem 物件陣列,我們可以這樣簡單編寫一系列的 LibraryItem 物件初始化:

var views: [LibraryItem] {
    LibraryItem(...)
    LibraryItem(...)
    ...
    LibraryItem(...)
}

若不使用 @LibraryContentBuilder,我們應該要明確地初始化並回傳一個 LibraryItem 物件的陣列:

var views: [LibraryItem] {
    var arr = [LibraryItem]()
    arr.append(LibraryItem(...))
    arr.append(LibraryItem(...))
    ...
    arr.append(LibraryItem(...))
    return arr
}

也就是說,@LibraryContentBuilder 讓我們可以更簡單明瞭地定義 LibraryItem 物件。

現在回到我們的範例,透過 Xcode 函式庫讓 MyProgressView 可重用最簡單的方式,就是初始化一個新的 LibraryItem 物件,並將一個新的 MyProgressView 實例作為參數傳遞給它。

struct MyProgressViewLibraryContent: LibraryContentProvider {
    @LibraryContentBuilder
    var views: [LibraryItem] {
        LibraryItem(MyProgressView(progress: 25, progressColor: .blue))
    }
}

上述作為參數給予 progressprogressColor 的預設數值,將會被 Xcode 標記化 (tokenized),所以在使用時很輕易能用其他有意義的值來替換。同時,這也提供了視圖初始狀態(25%的進度及藍色進度條)。

Xcode 一直在解析 LibraryContentProvider 客製化型別的原始碼,例如上面的 MyProgressViewLibraryContent,而且它會自動將任何正確指定的 LibraryItem 物件到函式庫當中。這過程不需要建置整個專案、關閉並重啟 Xcode,或是執行任何動作,新的項目馬上就會出現在函式庫當中!

為了驗證這件事,讓我們打開視圖函式庫,滑動到下方或在搜尋欄中找到我們的進度視圖:

librarycontentprovider-swiftui-xcode-12

你可以切換到 ContentView.swift 檔案,並刪除創建 MyProgressView 實例的那行程式碼(我們在先前加入的部分)。之後,打開函式庫並拖放我們的新項目,客製化視圖的初始狀態將會在 Preview 中顯示,有需要的話,你可以更改預設值,以便進度視圖可以依照想要的形式來運作。

librarycontentprovider-swiftui-xcode-12

你一定也注意到,我們的客製化項目在函式庫中有自己的類別:

swiftui-reusable-view

類別名稱就是該項目所屬專案的名稱,更精確地來說,是該項目所屬的模組名稱(稍後你就會明白)。同時,你也可以注意到項目的顏色是灰色。

雖然客製化項目看起來沒問題,我們還是可以讓它更加完美。我們可以讓函式庫標示出這個項目是屬於控制類別。

切換到 MyProgressView.swift 檔案中,並如此更新 LibraryItem

struct MyProgressViewLibraryContent: LibraryContentProvider {
    @LibraryContentBuilder
    var views: [LibraryItem] {
        LibraryItem(MyProgressView(progress: 25, progressColor: .blue),
                    category: .control)
    }
}

你可以看到我們提供了 .control 的值,給一個名為 category 的可選參數。我們有四種可選數值來設定項目,你可以按想要項目在函式庫呈現的類別來選擇使用:

  • control
  • effect
  • layout
  • other

最後的數值 other 是針對所有新項目系統自動使用的類別,除非我們像剛剛那樣特別做設定,否則都會是這個類別。把我們客製化項目為分類 control 之後,視圖函式庫就會如此更新外貌,包含了圖示顏色及類別標題:

librarycontentprovider-swiftui-xcode-12

指定函式庫項目的類別並不會影響項目的使用方式,這只是讓我們可以更精確地描述項目的種類而已。

按上面的做法,我們可以在 Xcode 函式庫中添加許多對應到 SwiftUI 視圖的項目。請注意,我們可以創建幾個 LibraryContentProvider 客製化類別,來連結到多個原始碼檔案。Xcode 將會解析它們,並且將任何找到的函式庫項目加入到函式庫當中。

備註:先前的教學中也簡單地說明過這個功能,當時我使用了一種不同的方式,來將所有函式庫項目集中到一個檔案之中。如果要使用這種方式,請參考該文章;否則,我們就要在客製化視圖存在的原始檔案中,實作客製化的 LibraryContentProvider  型別。兩種方法都可以達到我們的目的。在本篇文章的尾聲,選擇哪種方法已經不重要,因為我們會將所有東西都移到 Swift Package 之中,如此一來客製化函式庫項目就能夠在其他專案中被使用。

將進度視圖的變化版本 (variation) 添加為函式庫項目

試想像一下,我們現在希望在進度條下方,以文字形式顯示進度百分比,這算是原本客製化進度視圖的一種變化。而我們的最終目的,同樣是讓它成為函式庫裡面可用的項目之一。

首先,讓我們在 MyProgressView 視圖的實作中添加一點東西。在 MyProgressView.swift 檔案中,到 MyProgressView 結構的初始處、 progressprogressColor 下方加入下列屬性:

struct MyProgressView: View {
    var progress: Double
    var progressColor: Color

    // Add this property.
    var showProgressText = false

    ...
}

這是一個旗標 (flag),用於控制顯示進度的 Text 控件是否可見。在預設情況下,文字會被隱藏起來,因為它的初始值是 false

接著,在 body 的實作內部找到 // Progress text will be added here 的註解,並以下列這幾行程式碼作替換:

if showProgressText {
    Text("\(Int(getProgress()))%")
        .padding(.top, 20)
        .font(.callout)
}

這一小段程式碼簡單地將進度百分比的 Text 控件初始化,以整數以及 “%” 符號來代表百分比,並在上方加了 20pt 的間距,同時也指定了 callout 字型。以上這些東西,都只會showProgressTexttrue 的情況下顯示。

不要忘記我們要相應更新 VStack 的高度。找到位於 VStack 後面的 .frame(height: 20) 修飾器,並如此將它更新:

.frame(height: showProgressText ? 40 : 20)

要預覽我們在 MyProgressView 視圖中所變更的效果,讓我們到 MyProgressView_Previews 結構並作更新,讓它可以顯示第二個預覽,並將 showProgressText 設定成 true

struct MyProgressView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MyProgressView(progress: 40, progressColor: .green)
                .previewLayout(.sizeThatFits)
            MyProgressView(progress: 40, progressColor: .purple, showProgressText: true)
                .previewLayout(.sizeThatFits)
        }
    }
}

兩個預覽看起來分別會是這樣:

demo-preview

現在來到最有趣的部分,就是讓我們可以在視圖函式庫獲得這個進度視圖的變化版本。

同樣在 MyProgressView.swift 檔案之中,往下找到 MyProgressViewLibraryContent 客製化型別實作的位置。到目前為止我們在這裡初始化了一個函式庫項目,現在我們要來初始化另一個。

在第一個 LibraryItem 的下方加入下列程式碼。請注意,這次初始化的時候,我們需要將 showProgressText 參數設定為 true

struct MyProgressViewLibraryContent: LibraryContentProvider {
    @LibraryContentBuilder var views: [LibraryItem] {        
        ...

        LibraryItem(MyProgressView(progress: 25, progressColor: .blue, showProgressText: true),
                    category: .control)
    }
}

我們也可以提供客製化標題給函式庫項目,這也是一種簡短描述,讓我們可以輕易地分辨它們。

要為項目客製化標題,方法就如同剛剛設定類別一樣,只是我們用來做設定的參數是 title,在這個參數之中,我們可以提供任何想要的標題文字。請記住,在初始化一個 LibraryItem 物件的時候,title 引數 (argument) 會在 category 的前面。

讓我們為兩個函式庫項目添加合適的標題,這樣就可以輕易地區分它們。以下你會再看到 MyProgressViewLibraryContent 的實作,不過這次我們也設定了標題:

struct MyProgressViewLibraryContent: LibraryContentProvider {
    @LibraryContentBuilder var views: [LibraryItem] {
        LibraryItem(MyProgressView(progress: 25, progressColor: .blue),
                    title: "My Progress View", // Title for the original progress view
                    category: .control)

        LibraryItem(MyProgressView(progress: 25, progressColor: .blue, showProgressText: true),
                    title: "My Progress View With Progress Text", // Title for the progress view with text
                    category: .control)
    }
}

現在,我們可以打開函式庫確認一下新的項目:

img

只是加上標題這樣小小的改變,也足以讓我們更容易區分原本的進度視圖及它的變化版本。

你也可以在 ContentView 中試一下,更新一下就可以顯示進度數值,並在即時預覽中顯示。

img

使用 Swift Package 來重用及分享客製化視圖

假如你現在創建了一個新的專案,並試著在新專案的視圖函式庫中尋找剛剛新增的客製化函式庫項目,你大概會失望而回。視圖函式庫中沒有該函式庫項目的原因很簡單:因為 Xcode 是透過解析客製化 LibraryContentProvider 型別,並將其中找到的所有 LibraryItem 項目加入到函式庫裡面。新專案中並沒有包含任何客製化型別,也很明顯沒有實作 MyProgressViewLibraryContent 型別來使進度視圖能夠在函式庫當中被使用。

所以,我們得到的結論,就是可重用的 SwiftUI 視圖及客製化函式庫項目,預設只能在同一個專案中使用。聽到這消息你可能有點失望,但是不用擔心,因為這正是 Swift Package 派上用場的時候!

只要 Swift Package 包含了客製化視圖實作、和定義了函式庫項目的 LibraryContentProvider 型別,我們只要把 Package 加入到新專案之中,就可以重用 SwiftUI 視圖。Xcode 將會解析 Package 中的原始碼,並將任何找到的新項目加入到函式庫當中。除此之外,有了 Swift Package,就可以輕鬆地分享 SwiftUI 程式碼。現在,Package 多數都會放在像是 GitHub 的託管儲存庫當中,其他開發者就能夠拿到 Package,並在他們的專案中使用我們的視圖;同樣地,我們也可以這樣來重用其他開發者所製作的視圖。

如同上面所說,現在就讓我們來一步步創建一個包含客製化進度視圖的 Swift Package 吧。在接下來的幾節,我們會看到如何讓 Package 可以在專案中使用,並看看如何完全獲取 MyProgressView 視圖。

讓我們到選單 File > New > Swift Package…,在電腦中找個位置來儲存它。為了配合本篇教學文章,請將 Package 命名為 ReusableUI。然後,點擊 Create 按鈕來創造 Package。

完成後,我們就來刪除不需要的 Tests 資料夾:

img

下一步,打開 Sources > ReusableUI 資料夾,點擊 ReusableUI.swift 檔案,並將裡面所有內容刪除。我們不會用到這個檔案,但是它必須存在 Package 之中。

最後,在匯入 MyProgressView 實作之前,讓我們更新 Package.swift 檔案,讓它再沒有引用的 Tests 目標 (target)(因為我們刪除了 Tests 資料夾),更重要的是,為它指定 iOS 最低的需求版本,因為某些特定的 API 是從 iOS 14 開始才可以使用,像是 LibraryItemLibraryContentBuilder

以下就是更新後的整個 Package.swift 檔案,你可以從中找到剛剛提到的修正:

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ReusableUI",
    platforms: [.iOS(.v14)],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "ReusableUI",
            targets: ["ReusableUI"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(
            name: "ReusableUI",
            dependencies: []),
    ]
)

現在,將 ReusableViewsAndModifiers 專案與 ReusableUI Package 並排放置,選擇專案中的 MyProgressView.swift 檔案,並拖拉到 Package 內 ReusableUI.swift 檔案的下方。

img

原始檔案將會變更資料夾,並移動到 Package 資料夾底下,之後就不會存在於專案之中。而且你會發現,在 ReusableViewsAndModifiers 專案導覽列中, MyProgressView.swift 檔案名稱會變成紅色。這是正常的現象,我們只需要在紅色檔案上點擊右鍵刪除即可。

ReusableUI Package 現在已經包含了我們想要重用或分享的視圖,我們也可以加入更多像是 MyProgressView 這樣的實作到同一個 Package 中,稍後我們也會加入重用的修飾器進來。接下來,讓我們會將 Package 加入到範例專案之中,並且做一些必要的設定。

在專案中加入 Package

如果你現在嘗試去建置專案,你會就看到結果顯示失敗,因為 Xcode 找不到 MyProgressView,它已經被我們移動到 ReusableUI Swift Package 之中。不過別擔心,我們馬上就會來修正這個問題。

第一步,讓我們將 Package 加入到專案之中作為相依項目 (dependency)。因為 Package 就在本機裡面,所以這步驟很快就完成。如果 Package 是存在於網路上的遠端儲存庫,比如說 GitHub 上,那我們就需要在 File > Swift Packages > Add Package Dependency… 依照相關流程來加入 Package。

在你嘗試將 Package 加入專案之前,記得先把它關閉。然後,找到 Package 所在資料夾的位置,並將包含 Package 的資料夾拖放到 Xcode 的專案導覽列之中。

img

完成之後,Package 會在專案導覽列中,顯示一個我們能夠分辨的 Package 圖示。

備註:假如因為某些原因而導致 Package 無法顯示,或是遇到了無可預料的情形,請試著將 Xcode 關閉再重啟。很不幸地,這種情況不時會發生。

將 Package 加入到專案之後,打開 ReusableViewsAndModifiers 目標,並切換到 General 頁籤。在那邊找到標題為 Frameworks, Libraries, and Embedded Content 的段落,並點擊加號按鈕。在出現的視窗當中選擇 ReusableUI 函式庫,並點擊 Add 按鈕,這樣你應該就能在嵌入內容的清單之中看到所選的函式庫。

img

最後,我們需要在想要使用 Package 程式碼的地方中匯入 ReusableUI。因為 ReusableUI 與我們匯入的程式碼屬於不同模組,所以,讓我們打開 ContentView.swift 檔案,並加入以下敘述:

import ReusableUI

在進入下一步之前,請將 ContentView 本體中所有 MyProgressView 實例刪除,這樣我們就可以從零開始,加入一個或多個視圖到 content view 之中,並確保我們的 Package 可以正常運作。

現在打開視圖函式庫,往下滑動找到客製化進度視圖項目,或是直接透過 Xcode 搜尋 “progress”。你看到的畫面應該是這樣:

img

非常好!因為 Xcode 辨認出客製化函式庫項目所屬的模組(標註在類別裡),現在我們不需要其他步驟,就能夠在專案當中使用它們了!

讓我們將項目拖放到程式碼之中,以添加客製化進度視圖到 content view 裡。實際上,我們會利用初始化 LibraryItem 物件時指定的預設參數,添加進度視圖的實例。不過建置專案後,你會發現 Xcode 出現了下列錯誤:

Cannot find MyProgressView in scope

這絕對不是我們想要遇到的情況,所以讓我們進一步來瞭解問題所在,並且想辦法修正它。

修正進度視圖消失的錯誤

就算我們已經知道 MyProgressView 的實作存在 ReusableUI Package 之中,而且已經採取所有讓專案能夠使用 ReusableUI Package 的必要動作,但 Xcode 還是出現上述錯誤的原因是:

ReusableUI 是一個不同的模組,而任何實體(像是 MyProgressView 結構)在預設情況下都不是以 public 方式來宣告的,也就是說,對於其他模組來說是看不見的。同樣的規則也適用於我們需要訪問的 MyProgressView 屬性,它們也都必須以 public 方式來宣告。實際上,若沒有特別指定存取控制的權限,預設都會以 internal 的層級來標註,也就是說都只能在被定義的同一個模組下存取。

備註:如果你想瞭解更多關於 Swift 存取權限,你可以參考這篇文章,來瞭解更多關於模組的不同存取層級及可視範圍。

瞭解了錯誤是如何發生後,讓我們直接來修正它吧。要做的事情很簡單,我們需要將所有實體設為公開,使它們能夠在 ReusableUI 模組之外被找到。

在專案導覽列中,展開 ReusableUI Package 並找到 MyProgressView.swift 檔案,打開檔案就會看到原始程式碼,我們將會在一些地方加入 public 存取權限關鍵字,那麼就從 MyProgressView 的第一行開始:

public struct MyProgressView: View {
    ...
}

接著,對在結構中宣個的三個屬性執行相同的操作:

public var progress: Double
public var progressColor: Color
public var showProgressText = false

並在 body 屬性的第一行也加上 body 屬性:

public var body: some View {
    ...
}

現在我們差不多完成了,如果你試著按下 Cmd+B 建置專案的話,你會看到一個新的錯誤:

‘MyProgressView’ initializer is inaccessible due to ‘internal’ protection level

這是什麼意思呢?

考慮一下下面的情況:

MyProgressView(progress: 25, progressColor: .blue)

上面的程式碼呼叫了一個初始化方法(init 方法),來創造一個新的 MyProgressView 實例。儘管我們沒有在 MyProgressView 結構之中實際定義任何 init 方法,但結構會在後台自動為我們定義。這種情況在結構中很常見,預設的 init 方法每次都會隱含地被定義。此外,這也是我們不需要編寫任何關於初始化的程式碼,就可以創造並使用 SwiftUI 視圖的原因。

然而,預設的初始化器在這裡並沒有辦法符合我們的需求,它是使用預設的 internal 存許層級,因此沒辦法在這樣操作。我們希望初始化器能夠被公開,那麼就可以在 ReusableUI 模組之外創建 MyProgressView 視圖。為了處理這種情況,我們必須明確地定義 init 方法,並且標註為 public

將下列幾行程式碼加入到 MyProgressView 結構的屬性宣告和 body 實作之間:

public init(progress: Double, progressColor: Color, showProgressText: Bool = false) {
    self.progress = progress
    self.progressColor = progressColor
    self.showProgressText = showProgressText
}

現在 init 方法已經能夠從專案中被使用。重新建置專案,你會發現終於成功了!

在讀完這部分之前,請記住剛剛所做過的一切,請確認你已經將所有實體、屬性、方法、及初始化器標註為 public,以便你能夠在 Swift Package 之外使用它們。

備註:你可以在這篇文章閱讀更多關於 Swift Package 的實作

將修飾器加入為函式庫項目

到目前為止,我們已經完成了為客製化 SwiftUI 視圖創建新函式庫項目的步驟,並利用 Swift Package 讓視圖可重用於其他專案。

然而,在 SwiftUI 專案中,不只是 SwiftUI 視圖可以拿來重複使用或創建為函式庫項目,我們可以將整個流程應用在修飾器上,而這正是我們下半部教學的重點。

首先,我們來決定要重複使用的修飾器,並讓它們可以在 Xcode 修飾器函式庫中輕鬆被存取。通常,你會想要把一組客製化修飾器組合在一起,並讓它們都能被重複使用。在我們的專案之中,有一組修飾器能夠達到我們的目的。現在打開 ContentView.swift 檔案來找到這組修飾器。

body 實作的開始處,你會看到第一個按鈕的實作,而緊接著按鈕的下方,就有一連串用來變更外觀的修飾器:

.padding(8)
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)

上面的修飾器是用來:

  • 為按鈕加入 8pts 的間距
  • 將背景顏色設定為藍色
  • 將文字顏色設定為白色
  • 將圓角半徑設定為 8pt

我們可以在 Update Progress 按鈕上看到修飾器的結果:

img

假設我們希望能夠重用該組修飾器,而不需要再重寫它們,讓我們可以為其他按鈕設置類似的樣式,來看看我們應該怎樣做吧!

首先,我們需要擴展這些被修飾器變更的控制元件的類別,並在裡面新增一個方法,以利用這些修飾器來變更控制元件。以我們的範例而言,被變更的控制元件是一個按鈕,因此讓我們到 ContentView.swift 檔案的尾端加入下列擴展 (extension):

extension Button {

}

在這裡面,我們來定義一個新的方法,以使用我們想要重用的修飾器來修改按鈕的實例,讓我們將它命名為 roundedBlue()

extension Button {
    func roundedBlue() -> some View {

    }
}

你會發現我們透過這個方法回傳了 some View,而不是 Button 物件,所以在這個方法被呼叫後,還可以使用額外的修飾器。此外,Button 遵循了 View 協定,所以我們可以從方法回傳 some View

在方法裡面,我們將會回傳一個使用修飾器之後的按鈕實例,就像這樣:

extension Button {
    func roundedBlue() -> some View {
        return self
            .padding(8)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
    }
}

上面的程式碼還可以寫得更簡單,因為我們只有一個 return 敘述,所以可以直接省略 return 關鍵字:

extension Button {
    func roundedBlue() -> some View {
        self
            .padding(8)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
    }
}

我們剛剛實作的這個 roundedBlue() 方法,可以像其他修飾器一樣用在任何按鈕物件上。現在,讓我們將它做成一個函式庫的可用項目。

差不多完成了!讓我們在這裡創建一個新的客製化型別,它必須遵循 LibraryContentProvider 協定,以便為修飾器定義一個新的 LibraryItem 物件。我們會將這個新的型別命名為 ButtonMods。在 ContentView.swift 檔案裡剛剛擴展結束的位置,加入下列程式碼:

struct ButtonMods: LibraryContentProvider {

}

這次,我們不是要實作一個 views 屬性,而要要實作一個用來初始化並回傳包含 LibraryItem 物件集合的方法。這個方法在 LibraryContentProvider 協定裡的定義如下:

func modifiers(base: Self.ModifierBase) -> [LibraryItem]

這回傳型別是一個 LibraryItem 物件的陣列,不過更有趣的地方是參數的數值,這是修飾器正在修改的控制元件的型別,而該修飾器會在此方法中被創建為函示庫項目。

以我們的範例來說明,控制元件的型別就是 Button 型別,不過我們不能只是以 modifiers(base: Button) {...} 的方式來編寫。如果你到 Xcode 中查看 Button 結構的定義,你會看到我們必須將 View 型別以引數形式提供給 Button 泛型型別。這裡的視圖就是 ContentView

請記住上面的說明,現在讓我們在 ButtonMods 結構裡面定義 modifiers(base:) 方法:

struct ButtonMods: LibraryContentProvider {
    @LibraryContentBuilder
    func modifiers(base: Button<ContentView>) -> [LibraryItem] {

    }
}

你會看到 @LibraryContentBuilder 屬性在這裡也是必要的,這樣一來我們才能夠輕易地初始化 LibraryItem 物件,而不須以陣列的形式回傳它們。

在方法的本體之中,我們只會初始化一個 LibraryItem 物件,這也正是我們所要的。而這個項目程式碼段的參數值將會呼叫 roundedBlue() 方法,我們可以透過 base 參數值來獲取這個方法。然後,讓我們為項目設定類別與標題,這樣它在函式庫當中看起來會更清楚:

struct ButtonMods: LibraryContentProvider {
    @LibraryContentBuilder
    func modifiers(base: Button<ContentView>) -> [LibraryItem] {
        LibraryItem(base.roundedBlue(),
                    title: "Rounded Blue Button",
                    category: .control)
    }
}

現在,我們可以透過函式庫來重複使用客製化修飾器了!打開 Modifiers 函式庫,往下滑動或是搜尋 “button” 關鍵字:

librarycontentprovider-modifer-library

讓我們來看看它在 Reset Progress 按鈕上能不能夠正常運作。在 .padding(top, 40) 修飾器後面,接著呼叫 .roundedBlue():

Button(action: {
    self.progress = 0
}, label: {
    Text("Reset Progress")
})
.roundedBlue()
.padding(.top, 40)

重設按鈕就會更新為這樣:

img

你現在也可以將原本 Update Progress 的四個修飾器替換成 .roundedBlue(),結果也會是一樣,但原本我們需要四行程式碼,而現在只需要一行。

將 Modifiers 函式庫項目移動到 Swift Package 中

要在其他專案中重用我們的客製化修飾器,或是與其他人分享修飾器,我們必須將它加入到 Swift Package 當中,如同先前加入 MyProgressView 視圖的流程一樣。在前面的教學中,我們已經創建了 ReusableUI Package,並且將它作為依賴加入到範例專案當中,所以我們不用再另外創建一個新的 Package,直接使用 ReusableUI Package 就好。

首先,我們要在 ReusableUI Package 裡加入一個新的檔案。為此,到專案導覽列中將 Package 資料夾全部展開,你會在 Sources > ReusableUI 的資料夾下發現已經有兩個現存的檔案。接著,右鍵點擊 ReusableUI 資料夾,並選擇 New File 選項。

然後,在視窗中選擇 Swift File 並點擊 Next。

新的檔案將會被加到專案導覽列中,Package 裡兩個先前的程式碼檔案旁邊。在預設情況下,檔案名稱會是 “File.swift”,點擊一下便可以編輯名稱,我們將它命名為 RoundedBlueButton.swift

然後,把檔案裡預設的初始內容刪除,並加入下列的匯入敘述句:

import SwiftUI

下一步,我們要把剛剛實作的 Button 擴展和 ButtonMods 結構,移動到 Package 的 RoundedBlueButton.swift 檔案中。為此,讓我們回到 ContentView.swift 檔案,並剪下或複製 ButtonMods 結構和擴展。如果你是使用複製的方式,那就要將原本 ContentView.swift 檔案裡原本的實作刪除。

librarycontentprovider-swiftui-xcode-12

切換到 Package 裡的 RoundedBlueButton.swift 檔案,將剛剛剪下或是複製的內容貼到匯入敘述句的下方。

librarycontentprovider-swift-package

現在這裡出現了一個問題:在 modifiers(base:) 方法內,作為引數給予按鈕型別的客製化型別 ContentView 無法被編譯器所辨認,Xcode 跳出了下列錯誤:

Cannot find type ‘ContentView’ in scope

這是我們預計得到的結果,因為 ContentView 是在 ReusableViewsAndModifiers 專案裡被定義,但是 ReusableUI Package 是另一個模組,因此就沒有辦法找到它。但是除了這個原因之外,我們沒有其他需要將 ContentView 設為 public 的理由,要讓 ReusableUI 能夠「看到」它。雖然 ContentView 對我們的範例專案裡有意義,但是對於其他沒有這個視圖的專案來說呢?

其實在這裡有一個解決方法,讓我們可以完全擺脫對 RoundedBlueButton.swift 檔案內 ContentView 的引用。我們可以在檔案中創建一個新的 View 型別,並且將型別作為 modifiers(base:) 方法中的按鈕型別參數。儘管如此,繼續讀下去你就會明白,我們還是可以將 roundedBlue() 方法當做客製化修飾器,對任何視圖裡的按鈕使用。

那麼,在 RoundedBlueButton.swift 檔案中加入下列程式碼:

struct MyView: View {
    public var body: some View {
        EmptyView()
    }
}

MyView 就類似於佔位符,它不會提供任何功能性,因此一個 EmptyView 實例非常適合拿來放在裡面。

現在來到問題的所在位置,在 modifiers(base:) 方法中以 MyView 型別來替換 ContentView

func modifiers(base: Button<MyView>) -> [LibraryItem] {
    ...
}

最後,還剩下最後一個操作。還記得 ReusableUI 是一個額外的模組嗎?所以如果我們想要在它之外使用 roundedBlue() 方法為客製化修飾器,就必須將它標註為 public。幸運地,我們只需要更改一個地方,那就是 roundedBlue() 方法的定義:

public func roundedBlue() -> some View {
    ...
}

就是這樣!現在,讓我們建置專案,讓 ReusableUI Package 中的變動都能夠在專案中可見。如果你有一步步跟著文章操作,你應該不會遇到錯誤訊息。在 ContentView.swift 檔案中,客製化 .roundedBlue 修飾器應該能夠正常運作。

總結

我個人認為這個主題是我寫過最有趣的主題之一,希望你也是這樣覺得,因為這個技術能夠改善使用 SwiftUI 開發 App 的開發流程。

總結一下今天學到的東西,我們學會了在 Xcode 的視圖與修飾器函式庫中,把 SwiftUI 視圖與修飾器加入為可用項目,以創建可重複使用的視圖與修飾器。除此之外,我們還進一步學會了透過 Swift Package 將客製化函式庫項目打包,使它們可以在其他專案中被使用。

可以使用 Swift Package,來分享及發佈可重用的 SwiftUI 程式碼與函式庫項目,這件事非常令人興奮!這充分展現了 Swift Package 的優點,就是可以輕易地分享與嵌入到專案中。因此,來試試創建你的客製化可重用 SwiftUI 視圖、修飾器及函式庫項目吧!

譯者簡介:HengJay,iOS 初學者,閒暇之餘習慣透過線上 MOOC 資源學習新的技術,喜歡 Swift 平易近人的語法也喜歡狗狗,目前參與生醫領域相關應用的 App 開發,希望分享文章的同時也能持續精進自己的基礎。
LinkedIn: https://www.linkedin.com/in/hengjiewang/
Facebook: https://www.facebook.com/hengjie.wang/

原文:How to Reuse SwiftUI Views with LibraryContentProvider and Swift Package


資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。

blog comments powered by Disqus
Shares
Share This