Building a ProjectEuler 100 Challenge tracker in SwiftUI
TL:DR
I wrote a ProjectEuler100 Tracking app with SwiftUI! If you want to check out the code for the ProjectEuler Challenge tracker, visit my github page! Thanks to Quincy Larrson`s #ProjectEuler100 for the idea and Paul Hudson for the great SwiftUI tutorial! Check out both of their respective websites!
ProjectEuler100
Currently I am doing the #ProjectEuler100 challenge created by Quincy Larrson, the founder of freeCodeCamp. It consists of 100 mathematical algorithm problems that you have to solve programmatically. Because the challenges are quite tough, FreeCodeCamp named this challenge "the "Dark Souls" of Coding Achievements". You can find more information on the challenge here.
Coming up with the idea
So while I did these challenges, I always wanted to have a tracker to look at my progress. This should motivate me to keep going because. So I build a challenge tracking app using UIKit. While I developed this I was trying to learn SwiftUI. There is a great tutorial about everything SwiftUI written by Paul Hudson that I followed, feel free to check it out! :) ! After completing the app in the "traditional" UIKit way I've decided that Paul's tutorial gave me enough material to actually try and write the ProjectEuler Tracking App in SwiftUI!
Writing the app
The possibilities of SwiftUI are currently limited, so I stuck with the basic functionalities of the app when it came to features:
- show all challenges that I currently have to do and that I have completed,
- add the description of a challenge as a detail view that displays the actual project euler site with the task description,
- complete the challenge via a swipe in the challengelist
- add the possibility to reset all challenges back to their inital state
With that figured out I started developing the app with SwiftUI! Here is what I've learned so far.
Tables work like magic
So the first thing I tried to do was listing all the ProjectEuler challenges in a simple tableView. In UIKit this is not convenient because you always have to implement the behaviour with the actual displayed table, that is expressed in delegates and data sources. I cannot just say "Display my collection, please". This looks like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CELL_REUSE_IDENTIFIER) ?? UITableViewCell()
let section = self.sections[indexPath.section]
let eulerTask = section.eulerTasks[indexPath.row]
cell.textLabel?.text = "Challenge #\(eulerTask.id): \(eulerTask.name)"
cell.accessoryType = .disclosureIndicator
}
In SwiftUI this methodology changes significantly. The framework actually handles most of the internal process like the dequeuing of reusable cells for you. You "just" have to worry about designing your UI:
List {
ChallengeList(status: .todo)
ChallengeList(status: .done)
}
struct ChallengeList: View {
//...
let status: ChallengeStatus
var body: some View {
return Section(header: Text("TODO").font(.headline)) {
ForEach(state.challenges.filter { challenge in
challenge.status == status
}.prefix(10)) { challenge in
ChallengeRow(challenge: challenge)
}
}
}
//...
}
SwiftUIs approach to UI is way more declarative: You say what you want to see and how you want to see it. You don't have to care about internal behaviour. This enables us to display our requirements easily:
- A List containing two sublists of Challenges that either have the status "Todo" or "Done.
- Each sublist contains of "ChallengeRows" that are displaying the challenge number and name.
UIKit views can be used in SwiftUI
Great, this looks good! Now I want to display a WebView that shows me the description of a challenge from the project euler site. Turns out that this was actually my first challenge of this project: SwiftUI currently does not come with a native component for WebViews. Fortunately, Apple has implemented a bridge for that case in SwiftUI: UIViewRepresentables. They basically enable you to create your own SwiftUI view by incorporating classic UIViews and wrapping them in a protocol that SwiftUI understands:
struct WebView: UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
Not that our WebView component can be used in SwiftUI, we can directly address it in our ChallengeDetailView:
//...
var body: some View {
VStack {
WebView(request: URLRequest(url: URL(string: "https://projecteuler.net/problem=\(challenge.id)")!))
//...
SwiftUI is still "work in progress"
With the main flow of the application done, I want to add actions to my challenges. The main action should be the "complete action", that completes a challenge. In UIKit this can be done via UISwipeActions:
func tableView(_ tableView: UITableView,
leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
//...
let completeAction = UIContextualAction(style: .normal, title: "Complete") { _, _, _ in
_ = self.eulerTasksViewModel.complete(task: §ion.eulerTasks[indexPath.row])
self.updateSections()
tableView.reloadData()
//...
return UISwipeActionsConfiguration(actions: [completeAction])
}
SwiftUI does also support swipe actions. But these actions are limited. Only onDelete is actually supported natively. Therefore, I added the option to complete a challenge on our ChallengeDetailView as a navigationBarItem which is easy to do:
.navigationBarItems(trailing: NavButton(action: {
self.showChallengeCompletedAlert.toggle()
self.state.finish(challenge: self.challenge)
}, isVisible: challenge.status != .done))
I want to highlight the toggle() method here. It automatically toggles between both boolean values. This is very helpful because you don't have to set the right boolean value in the specific use cases anymore.
Shared state is a common pattern
With the challenges views finished the last thing I wanted to do was create a button that resets all challenges back to their original state. This includes creating a new view (SettingsView) and creating a button that triggers the resetting. Conceptually, I would have a problem now. The challenges were only used and maintained in the ChallengesView. Therefore, its state is only referenced in the ChallengesView. Fortunately SwiftUI has a concept for sharing state over multiple views or components: EnvironmentObjects!
var state = GlobalState()
// Create the SwiftUI view that provides the window contents.
let contentView = AppView().environmentObject(state)
EnviromentObejcts can be shared globally across all views of an app. You can instantiate them e.g. on a root level of your application like in the willConnect to method of the SceneDelegate. After that you can link the state in your view like this:
@EnvironmentObject var state: GlobalState
When the EnvironmentObject is updated, all views that are using it are rerendered. Therefore the changes that you make will directly reflect in your UI.
Conclusion
I hope this could give you an inside into writing simple iOS apps in SwiftUI. I cannot recommend Paul Hudsons SwiftUI tutortial enough when you want to have a nice introduction into SwiftUI. If you want to check out the code for the ProjectEuler Challenge tracker, visit my github page! And if you want to challenge yourself in these unique times checkout freeCodeCamps #ProjectEuler100