This article was written by me in early 2020, documenting my journey of relearning programming. Looking back now, although some of the technical understandings mentioned in the article may seem naive (please ignore the technical details in this article), it is still quite interesting. Coincidentally, the number of articles on my blog has reached 100, so I have brought this article back to remind myself of my original intentions.
-
The name of the app is Health Notes., Health Notes provides a powerful custom data type feature to meet the need of recording data for most of the health items in your life. You can create individual health data recording notes for each family member, or create corresponding notes for a specific item or a specific period of time.
Foreword
I started using computers at a relatively early age (my first computer model was the CP-80, with an MC6800 CPU), and I began learning programming early as well (starting with the Chinese learning machine and the domestic Apple II compatible machine). I have always been interested in computers and programming, but have never truly made writing code my profession. Although I have used several programming languages to write some code on different platforms, none of them could be considered a complete product. I habitually keep up with the latest trends in the information industry and some new technological directions, but due to business and other reasons, I have not touched programming at all since more than 10 years ago.
In the past 6-7 years, due to illness, my energy has been mainly focused on treatment. As someone who dislikes taking notes, I need to record a large amount of data every day due to treatment needs (although I mainly rely on my wife). Two years ago, through surgery, my illness greatly improved and I thought I would need to record less data. However, things did not go as planned. While the amount of data decreased, the number of data types greatly increased, and it is clear that these data will need to be recorded throughout my lifetime.
The lab report in the above figure is the blood test results within the past year.
I have also used spreadsheets to organize data before, but it was not convenient. With the abundance of apps available today, I have tried many of them to attempt centralized management, but the results were not satisfactory. Considering that my body has already recovered to a good state, I decided to try and create an app that meets my own needs. On one hand, it can exercise my brain, and on the other hand, it can help me regain my interest in the current field.
In the past decade, the development of information technology has been enormous, and many new technologies, methods, concepts, and previously unimaginable computing power upgrades have posed a significant challenge to me as an old rookie.
Selection of Development Platform and Framework
As a long-time user of Apple products (from Apple II-compatible machines, Apple II, grayscale PowerBooks, pseudo-color PowerBooks, several generations of iMacs, MacBook Pros, and numerous other Apple products), developing an app for use on iOS is a natural idea.
This app is mainly aimed at people like me who have a long-term interest in managing and analyzing health-related data.
I once considered using a cross-platform development framework, but ultimately chose native development for the following reasons:
- I don’t have any Android devices around me
- Limited ability and energy, unable to do large-scale adaptation
- When I had plans to develop, it happened shortly after the end of WWDC 2019, and Apple’s new SwiftUI, Combine, and new features in Core Data were very attractive to me
- Driven by interest, there is no commercial pressure or historical burden, so immature or uncertain technologies can be directly adopted
After some research, I finally chose the combination of SwiftUI + Combine + Core Data. There are both blessings and hardships, and there will be more explanations in the following text.
Technical Preparation
My biggest concern after not touching code for over a decade is not that my knowledge reserve is insufficient, but rather that I’ve lost my feel for it. Unfortunately, my worries were confirmed.
I’ve encountered many programming languages before, so understanding the basic syntax of a programming language isn’t a problem for me. The challenge lies in how to utilize its features. Although the learning cost of Objective-C is higher than that of Swift, after version 5.0, Swift has gradually become stable. Thus, Swift is currently a good choice for iOS development, as it’s faster and more efficient.
I bought a few beginner’s books on Swift (they were truly basic) and read them twice. I thought it wouldn’t be difficult to read other people’s code, but I found that I couldn’t understand much of it because most Swift code uses generics, functional programming, and other features that are only briefly mentioned in beginner’s books. Fortunately, I found some excellent books written by ObjCCN that helped me gain more knowledge and understanding of Swift. Although some of the materials were difficult to comprehend, I persevered, and the rewards were great.
After mastering the basic syntax of Swift, I began to learn SwiftUI. Initially, I read Apple’s official documentation and examples. As someone who lacks experience with UIKit, I didn’t immediately feel the benefits of declarative programming (my last programming memory was in the unreleased period of Django, when I first encountered the MVC pattern). It wasn’t until I needed to mix SwiftUI with UIKit to develop a particular feature that I truly appreciated the advantages of declarative programming.
The examples in Apple’s official materials were good, but when I tried to use them as a blueprint to create a complete app, I didn’t know where to start. Thus, I want to thank ObjCCN’s Wang Wei for his book on SwiftUI and Combine programming. I started reading it when it was available for pre-order, and I’ve benefited greatly from it. Specifically, I now have a more comprehensive understanding of the reactive ideas behind Combine. Strictly speaking, SwiftUI cannot be separated from Combine, but Combine can be integrated with other frameworks.
Compared to declarative thinking, reactive programming has brought me more shock. Especially in the later development process, as I deepened my understanding, I increasingly felt its charm.
With a certain technical foundation in implementing app UI, I started to consider the choice of data persistence with database.
I have used relational databases a long time ago, so I have some foundation. But I had no idea what kind of product or framework should be used on the mobile side. I thought that since it is an interest project, I can take the opportunity to learn more about popular mobile data persistence solutions. I read some NoSQL materials and also touched on some mainstream cloud data solutions, each with its own characteristics, and I couldn’t make up my mind for a while.
Actually, I planned to use Core Data at the beginning, but because there is not much information about it and the learning curve is steep, and also I feel that domestic app developers generally do not like to use it, so I temporarily put it aside. After repeated weighing, I still chose Core Data. First of all, its built-in cloud synchronization under iOS 13 is very attractive to me (basically free and cost-effective), and secondly, because Core Data is not an ORM (it should be called an object graph management framework), it has many other advantages in terms of performance and security.
There are many problems when learning Core Data, mainly reflected in the lack of information and difficulty in getting started. The best material I can find now is the Chinese version of Core Data created by objc, but to be honest, this book is not suitable for beginners to read. Through constantly searching for information online, watching YouTube videos, and studying the confusing explanations in Apple’s documents, it took me nearly a month to get started. As my understanding deepens, my fondness for Core Data also deepens. If your app does not intend to be cross-platform (only supports the Apple ecosystem), or you want to use native methods to manage local data under iOS, Core Data is really a very good choice. For example, for my app that runs only on iOS, excellent cloud synchronization can be achieved with minimal code overhead. In addition, if you make good use of Core Data’s features, you can get extremely convenient dynamic data management process under SwiftUI + Combine.
During the preparatory period, I also studied two video courses on DesignCode’s SwiftUI and Sketch, especially Sketch, which played a significant role in my later development.
Starting from the National Day holiday in 2019, I entered a learning state and by the end of November, I had gained the ability to build a complete app in about two months (in my opinion). From November 24th (the time of the first commit on Git), I officially entered the development stage.
Formal Development
Due to careful consideration of the requirements (knowing what kind of tools I need), progress in the first few days was very fast. SwiftUI provided me with a highly efficient environment, and the prototype of the entire app could be run in a short time. However, when it came to connecting the specific implementation and data flow together, I found that everything was not so simple.
There are several difficulties:
-
SwiftUI has limited functionality
When it comes to implementing many functions, it is found that many scenarios still need to be completed through UIKit. To this end, I spent some time learning about UIKit (at least how to mix the two together). In the final app, almost half of the display control was actually completed under UIKit, even the most basic requirement like TextField, sometimes the native version of SwiftUI couldn’t handle it.
-
There are too many bugs in SwiftUI and Combine
Although I was prepared for some imperfections in the new product, the number of bugs is far more than I expected. During the entire development process, I reported more than ten obvious bugs through feedback, and many strange phenomena that I could not reproduce with short routines. Anyway, after gradually figuring out the temperaments of these two “old men”, I can basically get along with these bugs peacefully.
-
Programming approach
Although I have the awareness of using the latest programming approaches and have been striving towards this direction in design and development, on the one hand, there is the inertia of previous experience, and on the other hand, my mastery of new ideas is still shallow. As a result, I took many detours in the entire development process. My data flow control logic was basically rewritten four times, and the current version of the code is half the size of the previous version, while being able to accomplish more functions, be more stable, and update the data in each view dynamically without intervention.
- Development environment
As someone who has not been involved in programming for many years, I still cannot fully adapt to the complex IDE tools of today for quite some time. In addition, some of Xcode’s error prompts are quite mysterious, with some being accurate and others being inexplicable, leading simple errors to strange places. It took me almost half a month to figure out what to trust and what not to trust.
In addition, package management, version control, and other aspects of development are all new subjects for me. Every time I encounter a new problem, it feels like a spiritual practice.
- App Store review
In this development, I intended to encounter more new challenges. I used various methods such as in-app purchases and automatic renewal in the app. Later, I found that I had dug a big pit for myself and was very lucky to finally solve it. The main problem is not technical, but because I have no experience in app review, I went through many wrong paths.
Overall, after a month and a half of development (including half a month of various review issues), the first version has now been launched on the App Store.
Although I have the awareness of using the latest programming ideas and have been striving towards this direction in design and development, on the one hand, it is the inertia of previous experience, and on the other hand, it is the shallow understanding of new ideas, which has caused me to take many detours in the entire development process. My data flow control logic has been basically rewritten four times. The current version has half the amount of code compared to the previous versions, while completing more functions, being more stable, and updating the data in each view dynamically without intervention.
Complaints, Tips, Experience, and Summary
This article is basically in a stream of consciousness state, writing whatever comes to mind. The following is about some problems, bugs, summarized skills, and a little bit of experience I encountered during development. There is no necessary order. If there are any errors, I hope everyone can offer constructive criticism 😅.
TabView
TabView in SwiftUi is a convenient control that can be used to easily implement standard screen bottom page switching with just a few lines of code. However, it has several issues:
- Poor control over the layout of items, which can be adjusted through some means but is not elegant;
- Switching pages will reset the state of the view. For example, if view1 has a ScrollView and you have scrolled it, when you switch to another view through TabView and then switch back, ScrollView will not stay in its original position but will go back to the top.
- Due to the reset when switching, efficiency is terrible when loading complex pages. When switching, TabView completely destroys the original view, and the efficiency of destruction is low. This causes the switching to flash like pressing the shutter of a mechanical camera if the page is complex. This is the most unacceptable issue among all the problems.
Originally, I thought about bridging to UIKit if it doesn’t work out, but in the end, I used the solution of simulating TabView functionality through ZStack. This solved the above problems and gained more control. Of course, there are also disadvantages. After using ZStack, all views are initialized and displayed even if they are not visible, and they will not be destroyed. Therefore, the means of intervention through onAppear and onDisappear are lost (other alternative methods have been adopted in the end).
ScrollView
ScrollView in SwiftUI follows the characteristics of other SwiftUI controls, making it very easy to use, but it hardly provides any additional control options. In my app, it is still competent in most cases, but when combined with certain UIKit implementations, it may result in strange phenomena. Finally, in some pages, using UIScrollView was the only way to solve the problem.
NavigationView
There are many strange phenomena.
The biggest one is that if the content of the view is complex and the barItem uses Chinese or images, when slowly sliding back from the left side of the screen, there will be overlapping NavigationBarItems at the top of different Views, causing the buttons of BarItems to fail. I have communicated with Apple through feedback several times for this bug, but because it is difficult to reproduce with simple examples, I almost wrote a small program alone to give them feedback. Currently, this bug has not been resolved, so in order to prevent users from experiencing problems (although it only occurs occasionally, it is also annoying), I temporarily shielded the function of sliding back from the left side of the screen in the app.
NavigationLink in the simulator can only be used once, and the second click will fail, but there is no problem on the actual device.
It is difficult to directly return to the root view. Using dissmiss can only return to the upper view. In the beta version of Xcode 11, some unconventional methods can be used to achieve this function, but they have now been blocked. This makes it difficult for me to implement the function of returning to the root view of a TabView by double-clicking on the icon, which is quite annoying. In fact, there is also a stupid way, which is to manage a View Stack by yourself, and then use onReceive to pass the dismiss command to the view layer by layer. However, because this multi-layer return is explicit, that is, there are complete animations, when it exceeds one layer, the user’s feeling will be weirder. Finally, I only used this method when the view was behind the ZStack.
Sheet
The problem is quite strange.
The environment value and environment object must be explicitly injected, otherwise it will cause runtime errors. From this point of view, Sheet should be isolated from other views in terms of data environment.
When a view has dynamically changing data based on ForEach, if the view is in a sheet, changes in data will cause exceptions to be triggered. If the view is extracted from the sheet and displayed directly, this problem does not occur.
Complex Forms in the same view will also cause exceptions when switching between Sheet and non-Sheet.
TextField
There is no problem with simple applications, except that multi-line input is not supported.
However, if the binding text of TextField is judged and processed in real time, the system’s built-in Chinese input method will not be able to input Chinese characters, while most third-party input methods do not have this problem. Finally, UITextView is used to solve the problem.
If TextField is in a ScrollView, when switching between different Segment Pickers, using the system’s built-in Chinese input method will cause a crash. There is no problem with English and third-party Chinese input.
It does not support hiding the input method after input, and requires a workaround through UIKit.
Text
There are no major problems, it’s quite easy to use, but the layout control is a bit weak.
Form
If if
is used in Form
to dynamically display content based on conditions, there may be strange events. The same code sometimes compiles normally, and sometimes does not. The same Form
code can sometimes compile normally in a Sheet
, but when moved out of the Sheet
, it compiles with errors, and sometimes the opposite is true.
List
List
has the ability to lazily load data, which is very suitable for scenarios with a large amount of data. However, its layout control ability is poor. If you must use methods similar to UITableView.appearance
to set up in init
, the settings in a single view will affect the entire app (unless you can control the initialization and destruction of that view very well). This is also a problem with control display settings in SwiftUI at present (mainly because the official does not recommend or support such behavior). If UIKit is used to modify settings in various views, they are not isolated from each other.
If there is an animation, the efficiency will be low when there is a lot of data, and using id
to force a redraw can solve this problem.
VStack HStack ZStack
The layout control is very convenient and can complete relatively complex layout construction in a short time. However, its support for ViewBuilder
types is relatively limited, and the Stack
that can be contained in a single Stack
cannot be too complex (currently, Apple has only defined a few forms of TupleView
). Especially for the Stack
after the if
, the requirements are strict. Sometimes, determining the some view
in the conditional branch can be extremely demanding, but sometimes it can be relaxed appropriately.
Picker
The categories are sufficient, but the details need to be further improved. The Segment must be fully displayed with animation before switching, giving a sticky feeling. The Date takes up a large area.
ForEach
The only way to control loops in view declarations, but the control needs to be strengthened.
If data: Range<Int>
is used, the range is immutable. For example, 0..<students.count
will result in game over if the number of students changes dynamically.
GeometryReader PreferenceKey, etc.
A good way for views to recognize themselves. With Geometry Proxy, sufficient space information can be obtained, and if information needs to be passed to the upper layer view, it can be completed through PreferenceKey.
Preview
Great idea, works great for simple view responses. However, once the view gets more complex, sometimes it’s more satisfying to just rebuild. Additionally, if the initial parameters of the view are complicated, like passing an NSManagedObject directly, building the preview is also difficult. Most of the time, it’s just deleted, but it’s very useful when building the framework.
Combine
Very useful and convenient. It also works well with many of the system’s built-in frameworks, worthy of system-level support.
The efficiency is currently problematic. Most of the controls in SwiftUI use the bind method to respond and pass data, which is designed to be very clear, but it can feel sticky to execute. Complex sheets are particularly noticeable. All asynchronous design responses will have some delay. I hope that in the future, there will be some optimization for the response. There are a few places in my app where the response to the sheet popping up is slow (moving the view out of the sheet and using NavigationLink to display it works well), especially when exiting. I feel that there is a serious efficiency bug in the code for destroying views in SwiftUI (see the TabView above).
Core Data
The new iCloud sync-based (not CloudKit) is very useful, and the setup is also very convenient.
The data in the cloud database in the app in the development environment does not communicate with the app data downloaded from the app store (with the same id). The data in the simulator during development cannot be synchronized with the data on the actual machine, and must be tested on multiple actual machines.
My app has a pure local database (no need for synchronization) and a synchronization database, each in different configurations. Relationships cannot be established between different configurations, after all, they are two different sqlite libraries.
Do not use Core Data like a database.
Relationship is a good thing, and the system will automatically maintain the relationship between data.
Thanks to Relationship, in most cases, there is no need to design primary keys yourself, which is a great convenience. However, when you need to manually export and back up the database, problems arise. You cannot use the system’s built-in primary key or ObjectID. Finally, identifiable properties were added to the Entite that needed to be exported. The normal program operation does not depend on this property at all, but when exporting JSON, these properties are used to mark their relationship.
In the managed context, the data execution efficiency is very high.
@FetchRequest is very good at dynamically managing data, and any changes in data in SwiftUI can be dynamically reflected.
@FetchRequest can only be dynamically set once through parameters in init (cannot be dynamically modified). If you need to display different predicates or sorting results, you can only reset them through the upper level view.
@FetchRequest cannot set more optimization parameters such as fetchLimit.
In-App Purchase
Technically, it is not complicated, because I do not need to set up a server for secondary authentication, so the logic is much simpler. The only thing is that Apple’s initial completion feedback after the purchase cannot guarantee that the user has paid, so it is necessary to query in the background to ensure that the payment is successful. There will be no prompts for refunds, so updating the receipt regularly is enough.
App Store Review
My review time was longer than usual because I didn’t fully understand how to use it. Most of the issues were related to in-app purchases.
Initially, I had missing metadata and had to resubmit a few times before realizing that I needed to include screenshots and notes for in-app purchases.
Later, I discovered that I also needed to select the in-app purchase options in the app submission process.
If I hadn’t made any changes at that point, it would have been fine. However, I made changes to the metadata for the pricing while the app was already under review, resulting in the app being rejected and the pricing staying in review status.
After waiting for a while, I deleted the original pricing data and created new pricing data, which was then approved for review.
I then filled in the new pricing data in the app submission and was rejected again.
The original description did not have a detailed explanation of in-app purchases, but after modifying it, it was finally approved.
The main reason for the long process was that each feedback took one day, and Apple’s responses were too vague, leaving me confused.
Other topics will be written later
Although there are still many shortcomings, the direction of SwiftUI + Combine is definitely correct and can bring significant efficiency improvements even now. I believe it will improve greatly in the next 2-3 years. CoreData is very useful, and native iOS programs should consider it more.
Conclusion
I accidentally wrote a lot, but it can be considered as a simple summary of these past few months.
Note: This article was written 3 years ago and many of the issues mentioned were caused by the framework’s immaturity and a lack of deep understanding of SwiftUI and Core Data at the time. Rereading the content now, I still have many feelings, but I also feel a clear sense of growth over the years.
This memoir documents the development of an iOS app using SwiftUI, Combine, Core Data, and other frameworks. The author shares their experiences with bugs, programming approaches, and the development environment, as well as tips for working with various controls and features like TabView, ScrollView, NavigationView, TextField, and In-App Purchase. The author concludes that SwiftUI + Combine is a promising development direction and that Core Data is highly useful for native iOS programs.
This article recounts how the author re-learned programming and developed iOS apps after turning 40, including the ups and downs of the learning process.