Cloning a Swift Package and Modifying ItThe source codeThe Swift PackageCreate A Test ProjectThe SwiftUI Segemented PickerInstalling the Swift PackageInspect the PackageThe FrameworkUsing the PackageClone the Package and add locallyModifying the CodeProperty MethodTestModifier MethodWorking with a ViewModel
Why would you want to install a local copy of a Swift Package and modify it? You may wish to simply pick through the code so you can get a better understanding of how things are done and improve your skills. I often do this by altering the code and seeing how it affects things. Perhaps the package is no longer being maintained and is not working with the latest OS, so it needs updating. Or, you may wish to improve upon the package for your own purposes.
When you use Swift Package Manager to install a Swift Package, you can inspect the code, but you can't make any modifications to it.
In this article, you will learn how to clone a Swift Package from a GitHub repository and add it to your own project as a local framework so that you can modify it.
Caution: If you are going to be cloning someone else's work then altering and using it for your own purposes, make sure that you read the license file to ensure that you are allowed to do that and identify any requirements there might be for giving appropriate credit
For this exercise, I am going to use code that was produced by Frank Jia in his article titled Build a Custom iOS Segmented Control With SwiftUI published on Medium at https://betterprogramming.pub/custom-ios-segmented-control-with-swiftui-473b386d0b51
I have made only minor modifications to the code to make it compatible as a Swift Package, and to add an accessibility property. Otherwise, I make no claim to the code, I am using it only as an example for my tutorial.
The swift package I created can be found at https://github.com/StewartLynch/SegmentedPicker but more on that shortly.
For this exercise, create a Swift project using SwiftUI and call it SegmentedPickerClone
We are going to be replacing the standard Segmented Picker that comes out of the box in SwiftUI with the custom one that Frank creates in his artcle. Then we are going to clone it and install it as a local framework and make some modifications.
The standard SwiftU picker of the type SegmentedPickerStyle can be created using the SwiftUI Picker constructor that requires a title, and selection bound to some value and an array of strings or views that you can use for the segment labels.
For example, you can create a @State property like this for selection
@State private var selection: Int = 0
The array of items could be an array of strings representing colors like this:
var colors = ["Red", "Green", "Yellow"]
We then replace that Hello World TextView with a Picker that is a segmented picker like this and make sure you apply the pickerStyle moderator designating it as .segmented
:
Picker("Pick your favorite color", selection: $selection) {
ForEach(0..<colors.count, id: \.self) { index in
Text(colors[index]).tag(index)
}
}
.pickerStyle(.segmented)
Frank's custom segmented picker provides us with a much easier syntax and it also opens up some real possibilities for our own customizations.
So let's get started.
Installing Swift Packages is extremely easy compared to installing a CocoaPod. This is all you have to do.
Copy the URL from the repository (https://github.com/StewartLynch/SegmentedPicker)
In Xcode, from the File menu, choose Add Packages...
In the search field, paste the copied URL. If the URL is recognized, it will be displayed along with the Dependency Rule.
Click on Add Package
After resolution, when the next screen appears, click on Add Package once more.
That's it. You have installed the package
If you want to inspect the package contents, drill down on the Package Dependencies until you reach the Sources/<PackageName> folder and inside there, you will find the code for the package. There is only one file for this package and we will come back to this later when we need to modify the code.
The package is installed as a framework and this means that it is in a separate module so all properties and functions that you need to access from your project must be public.
If you select your project target, and scroll to the Frameworks, Libraries, and Embedded Content section you will see that the framework has been added to your project.
Now it is time to use it.
Wherever you wish to use the package, you need to import it. So in ContentView, we want to replace the Picker we have right now with that new custom one called SegmentedPicker. So in ContentView, add
import SegmentedPicker
Replace the entire Picker you have in the body now (including the pickerStyle modifier) with an instance of SegmentedPicker.
When you do this, you see that it has three arguments, an String, an array of String and a binding to an Int.
For the label which will be used as the accessory lable, we can use the same string and for our items, we can use or colors array. The selection is bound to the selection property.
SegmentedPicker("Pick your favorite color", items: colors, selection: $selection)
This is a much nicer implementation and the segmented picker is arguably nicer too.
What I would like to do is to customize that picker by allowing the user to specify the color that they wish to use as the background behind the segments.
If we add a background modifier to our Picker right now, it just does not work.
SegmentedPicker("Pick your favorite color", items: colors, selection: $selection)
.background(Color.red)
The background of this picker (and a regular picker too) is the background of the entire view so this doesn't work.
If you read Frank's article you will see how he created this custom picker.
If we drill down to the SegmentedPicker source, we can see that there is a private static property called BackgroundColor that is assigned as the background for the custom picker.
What we want to be able to do is to modify that color so it can't be a static
property. However, when you try to do this, you will find that it is not possible.
Swift Packages are not modifiable. You need to "own" the framework and that is what the whole point of this tutorial is all about.
You could go to the GitHub repository and clone the package and add it to your project as a local framework, but there is a fast and easy way to do this.
Right-click on the package dependency and choose Show in Finder. This will open up the finder to the location where the package has been added. It is a pretty deep dive, but don't worry about that.
Right-click on the selected folder and copy it to your clipboard.
Back in Xcode, right-click on the project name at the top of the project navigator and choose Show in Finder and this will open the finder to the root level of your project.
Paste in the copied folder from step 2.
Return to Xcode and remove the Swift Package. To remove the package, you
Select your project name in the project navigator and then click on the Project.
Click on the Package Dependencies tab to view all dependencies.
Click on the one you want to remove and then click on the "-" button.
Bring up your root folder once more where you pasted it in step 4 and, drag and drop the folder from there into your project navigator.
This will add the project in as a framework.
Close the project.
I have had issues with source control when doing it this way, so I recommend that you remove source control entirely at this point and add it back later if you want.
To remove source control,
Open the new framework folder and enter ⌘-⇧-. (that is a period) and that will reveal hidden files.
Delete the .git folder
Enter ⌘-⇧-. once more to hide the hidden files again.
You still need to add the framework to your target so open the project once more and select your project name in the navigator then select your target and scroll to the Frameworks, Libraries, and Embedded Content section.
Click on the + button and the list of frameworks and libraries will be displayed. Select your new framework from the workspace section and click on Add and this will add it to your target.
You should now be able to build and run your project just as before. The difference is that now, you will be able to modify the code.
What I am going to do next is show you two different ways in which you can modify that background of the picker.
The first way that you can do this is by making that backgroundColor property in the SegmentedPicker code available to be modified if we want, from the project
Open the SegmentedPicker.swift file from the Sources/SegmentedPicker folder in the framework.
Change the static property with the initial value into a variable of the same name, but do not assign the default color here.
private static let BackgroundColor: Color = Color(.secondarySystemBackground)
TO
var backgroundColor: Color
The first time you edit the file, you will be asked to unlock it. What we are doing is replacing the static property with a variable one. In order for this to be accessible when we create the instance of SegmentedPicker from outside of this framework, we need to modify the initializer for the struct which is marked as being public. It is here where we assign that default color.
Change the initializer to the following:
public init(_ label: String, items: [String], selection: Binding<Int>, backgroundColor: Color = Color(.secondarySystemBackground)) {
self.label = label
self._selection = selection
self.items = items
self.backgroundColor = backgroundColor
}
Replace the background property of the body with this new value.
xxxxxxxxxx
from
.background(SegmentedPicker.BackgroundColor)
to
.background(backgroundColor)
Return to ContentView and if you build, you will see that nothing has changed. However, if you type a comma after the second argument in SelectedPicker, you now see that you can add a third argument and that is backgroundColor so do that and pick a different color.
xxxxxxxxxx
SegmentedPicker(items: colors, selection: $selection, backgroundColor: .green)
This is not very "SwiftUI-ish" in that in with SwiftUI, you are less likely to pass additional arguments into a view. You normally see this done with a modifier like "backgroundColor" that we can modify and force an update the view.
So let's remove that code in the initializer.
Set the initial value now where we create backgroundColor.
xxxxxxxxxx
var backgroundColor: Color = Color(.secondarySystemBackground)
Since it is now initialized with a value, we no longer need it in the initializer.
Create a new public function called backgroundColor that has a color property as a parameter and return a SegementedPicker view.
xpublic func backgroundColor(_ color: Color) -> SegmentedPicker {
}
In the body of the function, create a property called view and assign self to it.
Set the backgroundColor property to the color argument passed in, and finally, return the updated view.
xxxxxxxxxx
public func backgroundColor(_ color: Color) -> SegmentedPicker {
var view = self
view.backgroundColor = color
return view
}
Return now to the contentView and remove the now, not accessible backgroundColor from the initializer and add a new .background modfier to the SegmentedPicker view passing in some color.
xxxxxxxxxx
SegmentedPicker("Pick your favorite color.", items: colors, selection: $selection)
.backgroundColor(.green)
That's it.
If you want to work with a viewModel using MVVM and an Observable object, let's see how we can do that here.
Create a new swift file called MyColor and create a struct that we can use as our model and change the import to SwiftUI.
xxxxxxxxxx
import SwiftUI
struct MyColor {
var name: String
var color: Color
}
Inside the struct, create a static property called mockColors that returns an array of these MyColor objects like this:
xxxxxxxxxx
static var mockColors: [MyColor] {
[
MyColor(name: "Red", color: .red),
MyColor(name: "Green", color: .green),
MyColor(name: "Yellow", color: .yellow)
]
}
Create a new swift file called ViewModel and create a class called ViewModel that conforms to the ObservableObject protocol. Also change the import to SwiftUI.
xxxxxxxxxx
import SwiftUI
class ViewModel: ObservableObject {
}
Create a property call colors that is an array of MyColor and initialize it as an empty array.
xxxxxxxxxx
var colors:[MyColor] = []
Note: if you are going to be updating this from your view, it should be decorated with the @Published property wrapper, but we are not, so there is no need here because we are going to assign it in the initializer.
Create an initializer that will assign the MyColor.mockColors to this property
xxxxxxxxxx
init() {
colors = MyColor.mockColors
}
Create a Published property called selection, that is an Int, initialized at 0, the default segment.
xxxxxxxxxx
@Published var selection: Int = 0
Create another var that is called names and initialize it as an empty array of string
xxxxxxxxxx
var names = [String]()
In the initializer, map the names from colors array to that property
xxxxxxxxxx
init() {
colors = MyColor.mockColors
names = colors.map{$0.name}
}
Create one more computed property called selectedColor of type Color and return the color property of the MyColor item in the colors array specified at the index of the selection.
xxxxxxxxxx
var selectedColor: Color {
colors[selection].color
}
Return to ContentView now and replace the two properties with a single StateObject to initialize the viewModel
xxxxxxxxxx
@StateObject var vm = ViewModel()
Use the Viewmodel properites now for the SegmentedPicker.
xxxxxxxxxx
SegmentedPicker("Pick your favorite color.", items: vm.names, selection: $vm.selection)
Use the viewModels's selectedColor as the backgroundColor modifier's color.
xxxxxxxxxx
.backgroundColor(vm.selectedColor)
Now, when you and select a segment, the background of the picker will take on the color of the selected segment.
Nice, simple and readable code.
struct ContentView: View {
@StateObject var vm = ViewModel()
var body: some View {
SegmentedPicker("Pick your favorite color.", items: vm.names, selection: $vm.selection)
.background(vm.selectedColor)
.padding()
}
}