Showing posts with label iOS Development. Show all posts
Showing posts with label iOS Development. Show all posts

Wednesday, October 29, 2025

My iOS App: Tap to Capture the Moments

Background Story

It all started with a conversation in a social group long time ago.

We were discussing a tragic event — a moment that led us to reflect deeply on the meaning of life.

I said something that surprised even myself:

“I’ve written an iOS app. Maybe it could help people — maybe even save a life.”

Someone asked, “Tea Director, what kind of app is it? Could you tell us more?”

I smiled and replied, “Someday, when the time is right.”

That night, the topic was too heavy to shift away from.

Now, after quite some time, deeply believing in my iOS app, I’ve finally decided to write this post — to share with my friends what I consider one of my finest creations:

TapMyNotes — Tap to Capture the Moments.

The Journey Begins: A Hike in the Rockies

Let me start with a story — a mountain hike that inspired this reflection.

On October 7, 2025, a Tuesday, I went hiking in the Canadian Rockies with my wife and a couple of our best frieds, longing to see the golden larch needles of autumn.

As usual, once we parked near the trailhead, I opened my iPhone, launched my own app which I have being loved and trusted for many years, and tapped on a button in a view — recording the very start of our hike.

Now, days later, sitting at home, I opened that same record.

A map view of the coordinates, a visual feature of the app, exactly where we began.

It felt simple yet powerful — a small mark of time and place preserved, quietly, inside my app.

About the App

The app is called TapMyNotes (Chinese: 点击美好记录).

It supports both English and Chinese (Simplified and Traditional).

The idea is simple yet meaningful: with just one tap, you can instantly capture

  1. When,
  2. Where, and
  3. What

the three essentials I call the 3W Record.

The app’s data model is natural, open and flexible.

You can define your own structure in two levels:

  • Tap Entities – broad categories like Life, Work, Health, Outdoor, Travel, etc.
  • Tap Items – specific topics within each, like Running, Hiking, Swimming, Skiing, etc.

To record something, just select a tap item, open its info button, and tap the hand icon in a popup view.

The app automatically captures time and location, which cannot be changed.

That’s intentional — I want each record to reflect a true moment.

If you forget to tap, that moment is gone forever — because life only happens once.

Every tap becomes a conscious choice, a moment you decided was worth keeping.

Adding the Words

After the tap, the app reminds you to write — a note, a thought, a few simple words.

You can type, speak, or scan text; you can always edit or add more later.

I encourage users to write something immediately after the tap.

Those few words often capture your most genuine feelings.

Later, when you revisit and expand them, they’ll form the truest memories — written proof of what you felt in that moment.

Words breathe life into data; they give warmth to time and place.

Another reason I emphasize on the importance of a note is that words might be the key to find out your past taps by the search feature of the app, when you have hundres, thousands of tap records.

My Mountain Records

Nearly ten years ago, I started using my own app to record meaningful moments in my life.

My very first record was on May 27, 2017, a Saturday.

Since then, I’ve used it to document everything — from ideas to travels, from daily reflections to outdoor activities, including mountain hikes.

For hiking, I first tried naming each mountain as a separate Tap Item.

But soon, I realized that created too many entries.

So I created one item called “Mountain Hiking”, and used keywords within notes to distinguish each hike.

For total of 363 taps, if each hiking records 20 taps on average, this tap item contains about 18 hiking activities.

At the parking lot, start point, I added a key word of Vista Park 25/10, and a brief description about our hike.

Every time I hike, I record:

  • The starting point
  • Key scenic, event, or turning points or directions along the way
  • The summit or destination
  • And finally, the return to the starting point

Each record includes timestamp, GPS coordinates, and a short description.

These points connect on the map, forming a clear trail line. Mapview is one of key features in the app.

The app also calculates duration, distance, pace, and elevation gain — all essential for mountain activities, maybe usesful for other events or moments.

During hikes, I announced data to my companions from time to time:

“We’ve been walking for eighteen minutes — straight line distance 85 meters from the last stop, altitude 1,879 meters, up 29 meters!”

We walked deep in the woods and mountants. With the convinience of mobile phone and my app, we felt not only more confidence, but also a lots of fun.

Reflections on Data and Meaning

Every piece of data belongs to the user.

All information is stored locally, allowing full control and privacy.

Users can view details of each GPS location point associated to a tap — coordinates, intervals, elevation changes — or calculate the total ascent and duration between waypoints.

These records, precise and personal, aren’t just statistics.

They become stories, memories, and traces of a life well-lived.

Even years later, when place names change, the GPS coordinates remain — timeless reminders of where we once stood.

That’s why I treasure every tap record created with TapMyNotes.

It’s more than an app; it’s a way to keep your footprints visible across both time and space.

The following is the mapview of our last mountain hiking activity: Vista Lake and Arnica Lake

Vista Lake

and Arnica Lake

Afterthought

Since releasing the app, I’ve continued to refine it — adding new features, improving performance, and learning from users and friends who generously test and share feedback.

Many of them call it “a truly valuable app,” and I’m deeply grateful for their trust and encouragement.

Together, we’ve turned a personal idea into a shared journey of creativity and reflection.


I’ve been consulting ChatGPT for months on various design ideas and coding challenges. Many times, ChatGPT confirmed my thoughts and inspired me with ideas I hadn’t considered before for my requests many times.

With ChatGPT’s help, I’m now close to releasing my next update version — and through this experience, I believe that ChatGPT already knows my app quite well.

After finishing this draft, I asked ChatGPT to review it and craft a poetic ending, which I could not write.

I was deeply impressed by how well ChatGPT understood my intention and how beautifully it wrote this conclusion piece.

By the way, I didn’t use ChatGPT’s version word for word. No one understands TapMyNotes better than I do, and some things can only be said from the heart. So I made a few revisions in my own voice to bring out what this app truly means to me.

Conclusion by ChatGPT

A Moment in the Mountain —
Light in the Shadow

Deep in the mountains, the wind fell silent.
Golden needles were scattered along the trail.
Up and down the slopes, the mountain’s shadow
slowly pushed the sun away.

For a moment, I was not sure —
where I was, how far from the parking lot,
and how far to my destination.
Then, I took out my phone and tapped.

A signal appeared, faint yet steady.
The map glowed softly — a blank base of color,
but thin lines of blue and red
connected where I stood
to the previous point,
and to where I had begun.

That line was more than direction —
it was reassurance.
And the numbers, more than data —
they told a story of progress and endurance,
a quiet warmth in the cold air.

Those numbers showed where I was
and how my energy had been spent.
Though small compared to the mountains,
I felt great — accomplished, so far and so high.

Every tap, I realized,
is a promise to remember.
Each one says:
“I was here — in this place, at this moment.”

The shadow faded,
the mountain fell back into silence,
and I followed those colorful lines
back into the sunlight.

Epilogue

TapMyNotes is not just a note-taking app.
It is a way to mark your present existence —
to remind yourself that
  every moment in time axis,
  every breath in the universe,
  every step along your path,
  and every effort you’ve made,
are worth recording.

Other Blogs

Read More...

Sunday, May 25, 2025

Accessing iOS App Files

iOS apps are built on a sandbox infrastructure, which normally makes it difficult to access the files within an app.

However, as an iOS developer with an app on the App Store, I’ve found a way to view my app’s contents using the Xcode application.

In Xcode, go to Window → Devices and Simulators to open the relevant window. You should see your connected iPhone there. In the Installed APPS panel, click the + button to add your app if it isn’t already listed. Once added, your app will appear in the list.

To view the contents of the app, you need to download it first. Click the ... button next to the app name and select Download from the options.

Once downloaded, you can view the package contents from Finder. The file I’m most interested in is the core database of my app. From here, I can easily examine all supporting files, including the database itself. Finder allows me to open the files I need and verify their contents.

This is an extremely valuable and convenient tool for iOS developers. It allows me to verify, back up, and review the app’s contents on my iPhone before making further changes. I can also modify and replace content for testing purposes, and then incorporate those changes into my Xcode project after thorough testing.

For example, I’ve recently been working on an update to migrate from a single database store to multiple database stores. This change lays the foundation for future features and improvements in data access efficiency. However, it involves a major overhaul of the project, affecting many parts of the codebase. I was concerned about testing these changes on my iPhone, especially because I didn’t want to risk losing 18 years of valuable collected data. With this tool, I can now safely back up my app by selecting Download container... before running any tests. If anything goes wrong, I can easily restore the app using the Replace container... option.

References

  • Try searching for "access iPhone app files on Mac from Xcode". As of May 25, 2025, the top results (possibly generated by AI) provided exactly the guidance I needed to begin my research.

Other Blogs

Read More...

Wednesday, April 09, 2025

A Conversation with ChatGPT

Recently, I had a very good conversation with ChatGPT about a topic that often goes unnoticed: how to name a variable in a Swift class in my iOS app. This isn't the kind of question you typically search for on Google or post on StackOverflow (where you might have to wait a day or longer for a response). Yet, I was stuck trying to find a meaningful name—one that clearly conveys its purpose and is easy to recognize later during maintenance.

My Initial Question

ChatGPT came to mind. I explained why I needed this variable, and with some context, I hoped it would suggest a good name. Here's my original question:

I have a main view with some tab bars, such as Edit, Tap, and Report. The main view is the starting point. I would like to define an enum type to indicate where the starting point is, for example, TabBarStatus. I also like to define a variable for the status. What names should I use for the enum and variable? The variable will be used in subsequent views to find out where the start view is.

Less than a second later, ChatGPT responded with a thoughtful explanation and code examples.

For your use case, where you want to track which tab was the starting point (Edit, Tap, Report, etc.), defining an enum and a variable to hold that state is a great approach. Here's a naming suggestion that is clear, consistent with Swift/iOS conventions, and scalable:

🧩 Enum Name

Use a descriptive enum name that reflects the context:

  1. enum MainTab {
  2.  case edit
  3.  case tap
  4.  case report
  5. }

Alternative names:

  • StartTab
  • MainTabType
  • TabBarStatus (your suggestion, but slightly less Swift-like)

Swift style prefers concise, noun-based enum names like MainTab or StartTab.

🧩 Variable Name

For a variable that stores the starting tab, here are a few clean suggestions:

  • startingTab: MainTab
  • originTab: MainTab
  • entryTab: MainTab
  • launchTab: MainTab
  • startFromTab: MainTab (if used in routing context)
✅ Example Usage
  1. enum MainTab {
  2.  case edit
  3.  case tap
  4.  case report
  5. }
  6. struct ViewModel {
  7.  var startingTab: MainTab
  8. }

My Follow-Up

Just like talking to a senior developer, I continued the conversation and explained more about the context and how this variable would be used:

Good. Another question I need your advice on. All main view controllers and subsequent view controllers (VCs) are based on a base view controller, BaseViewController: UIViewController. All the subsequent VCs are presented as detail views, modal views, etc. I would like to define a public variable in BaseViewController, and its value is set in the main view controllers. I think this var should be static, instead of an instance variable. The reason is that there is only one starting tab. Right?

I was hoping ChatGPT could offer a better design based on my explanation. To my surprise, ChatGPT not only understood my design but also laid out a solid analysis of how the design could be improved.

ChatGPT's instant reply

Great question—and you're thinking in the right direction! Let's break it down and figure out the best approach.


🧠 Your Goal

You want all view controllers (which inherit from BaseViewController) to know where the user started (i.e., which main tab). There's only one starting tab per app session, and this info is set by the main view controller.


✅ Static vs Instance Variable
  • Static variable (static var or class var) is shared across all instances of BaseViewController. Perfect for global, app-wide state.
  • Instance variable (var) is unique to each instance, so you'd need to pass it manually to every view controller—probably not what you want here.

👉 Since there's only one starting tab, and you want all view controllers to access the same value, a static variable is the right choice.


✅ Suggested Implementation
  1. class BaseViewController: UIViewController {
  2.  // Default value, set from main controller
  3.  static var startingTab: MainTab = .tap
  4. }

Then, in your main tab view controllers (e.g. EditViewController, TapViewController, etc.), you set it like this:

  1. override func viewDidLoad() {
  2.  super.viewDidLoad()
  3.  BaseViewController.startingTab = .edit  // or .tap or .report
  4. }

And any other view controller can read it like:

  1. let origin = BaseViewController.startingTab


✅ Optional: Make It a Computed Property (if you want read-only from others)
  1. class BaseViewController: UIViewController {
  2.  private static var _startingTab: MainTab = .tap
  3.  public class var startingTab: MainTab {
  4.    get { return _startingTab }
  5.    set { _startingTab = newValue }
  6.  }
  7. }

Let me know if you also want to persist this value (e.g. between launches) or just keep it in-memory for one app session.

A Better Design

During this very natural and productive conversation, three concepts caught my attention: state, persistence, and computed property. They suddenly lit up my thinking like a flash of inspiration. A new design came to mind, and I shared it with ChatGPT:

There may be some cases where a subsequent view is not inherited from BaseViewController. I think it may be better to define a static class, such as MainTabStatus (or maybe a better name?). Within the class, a private var _startingTab and a public computed property startingTab can be used to manage the starting tab status. This class can be extended to include mechanisms to store the status in memory or user settings (persisting across app launches). This helper class can then be accessed from any view controller, view, or class to retrieve the starting point status.

Very quickly, I got feeback from ChatGPT.

Yes, that’s a great idea—and a cleaner and more modular design! Creating a dedicated MainTabStatus (or similarly named) helper class decouples the logic from your view controllers entirely, making it more flexible, easier to test, and extendable for future needs (like persistence).


✅ Suggested Design
  1. enum MainTab {
  2.  case edit
  3.  case tap
  4.  case report
  5. }
  6. final class MainTabStatus {
  7.  private static var _startingTab: MainTab = .tap  // default value
  8.  public static var startingTab: MainTab {
  9.    get { return _startingTab }
  10.    set { _startingTab = newValue }
  11.  }
  12.  // Optional: Save to UserDefaults if needed
  13.  public static func persist() {
  14.    UserDefaults.standard.set(_startingTab.rawValue, forKey: "StartingTab")
  15.  }
  16.  public static func restore() {
  17.    if let raw = UserDefaults.standard.string(forKey: "StartingTab"),
  18.       let tab = MainTab(rawValue: raw) {
  19.          _startingTab = tab
  20.    }
  21.  }
  22. }

🔧 For the UserDefaults part to work, you'd make MainTab conform to RawRepresentable (usually with String), like this:

✅ Suggested Design
  1. enum MainTab : String {
  2.  case edit
  3.  case tap
  4.  case report
  5. }

✅ Usage in Any View Controller

Set at startup:

  1. MainTabStatus.startingTab = .edit

Read anywhere:

  1. let origin = MainTabStatus.startingTab

Restore from UserDefaults (if persisted):

  1. MainTabStatus.restore()

🧠 Alternative Names for MainTabStatus

If you want something shorter or more Swift-y:

  • AppNavigationContext
  • StartContext
  • OriginTab
  • TabTracker
  • MainTabTracker

MainTabStatus is already clear, so it's a solid choice.


Let me know if you'd like this helper to also handle notifications (e.g., broadcast when tab changes), or if you want a sample using @AppStorage (SwiftUI), too!

Wow! What a great set of suggestions from ChatGPT—starting from a simple question about naming a variable, and evolving into a much better and scalable design.

Other Blogs

Read More...

Tuesday, August 01, 2023

WWDC 2023 Video Sessions

Apple's WWDC 2023 is over. In the past month, from Jun 6 to Jul 23, I have watched all videos published by Apple on the web. The following is a complete list of videos I have watched.

  1. Keynotes
  2. Platforms state of the union
  3. 17 big & little things at WWDC23
  4. What Apple developers need to know at WWDC23
  5. Add SharePlay to your app
  6. Bring widgets to new places
  7. Bring your game to Mac, Part 1: Make a game plan
  8. Bring your game to Mac, Part 2: Compile your shaders
  9. Bring your game to Mac, Part 3: Render with Metal
  10. Build custom workouts with WorkoutKit
  11. Build great games for spatial computing
  12. Build spatial experiences with RealityKit
  13. Create a great ShazamKit experience
  14. Create accessible spatial experiences
  15. Create rich documentation with Swift-DocC
  16. Debug with structured logging
  17. Design and build apps for watchOS 10
  18. Design for spatial input
  19. Design for spatial user interfaces
  20. Design spatial SharePlay experiences
  21. Detect animal poses in Vision
  22. Develop your first immersive app
  23. Discover Observation in SwiftUI
  24. Discover String Catalogs
  25. Discover machine learning enhancements in Create ML
  26. Enhance your app’s audio experience with AirPods
  27. Enhance your iPad and iPhone apps for the Shared Space
  28. Expand on Swift macros
  29. Explore App Store Connect for spatial computing
  30. Explore enhancements to RoomPlan
  31. Generalize APIs with parameter packs
  32. Get started with building apps for spatial computing
  33. Integrate your media app with HomePod
  34. Lift subjects from images in your app
  35. Make features discoverable with TipKit
  36. Meet ARKit for spatial computing
  37. Meet ActivityKit
  38. Meet MapKit for SwiftUI
  39. Meet Reality Composer Pro
  40. Meet Safari for spatial computing
  41. Meet StoreKit for SwiftUI
  42. Meet SwiftData
  43. Meet SwiftUI for spatial computing
  44. Meet UIKit for spatial computing
  45. Meet watchOS 10
  46. Perform accessibility audits for your app
  47. Principles of spatial design
  48. Ready, set, relay: Protect app traffic with network relays
  49. Run your iPad and iPhone apps in the Shared Space
  50. Spotlight your app with App Shortcuts
  51. Support Cinematic mode videos in your app
  52. Support HDR images in your app
  53. Sync to iCloud with CKSyncEngine
  54. Tune up your AirPlay audio experience
  55. Unleash the UIKit trait system
  56. Unlock the power of grammatical agreement
  57. What’s new in SF Symbols 5
  58. What’s new in StoreKit 2 and StoreKit Testing in Xcode
  59. What’s new in Swift
  60. What’s new in SwiftUI
  61. What’s new in UIKit
  62. What’s new in VisionKit
  63. What’s new in Xcode 15
  64. What’s new in managing Apple devices
  65. What’s new in web apps
  66. Wind your way through advanced animations in SwiftUI
  67. Write Swift macros
  68. Beyond scroll views
  69. Bring widgets to life
  70. Bring your Unity VR app to a fully immersive space
  71. Build accessible apps with SwiftUI and UIKit
  72. Build an app with SwiftData
  73. Build programmatic UI with Xcode Previews
  74. Build widgets for the Smart Stack on Apple Watch
  75. Create a more responsive camera experience
  76. Create animated symbols
  77. Create immersive Unity apps
  78. Create seamless experiences with Virtualization
  79. Deliver video content for spatial experiences
  80. Deploy passkeys at work
  81. Design widgets for the Smart Stack on Apple Watch
  82. Design with SwiftUI
  83. Discover Calendar and EventKit
  84. Discover Continuity Camera for tvOS
  85. Enhance your spatial computing app with RealityKit
  86. Evolve your ARKit app for spatial experiences
  87. Explore 3D body pose and person segmentation in Vision
  88. Explore Natural Language multilingual models
  89. Explore SwiftUI animation
  90. Explore advances in declarative device management
  91. Explore enhancements to App Intents
  92. Explore immersive sound design
  93. Explore materials in Reality Composer Pro
  94. Fix failures faster with Xcode test reports
  95. Get started with privacy manifests
  96. Inspectors in SwiftUI: Discover the details
  97. Meet Core Location for spatial computing
  98. Meet Object Capture for iOS
  99. Meet Push Notifications Console
  100. Meet RealityKit Trace
  101. Meet mergeable libraries
  102. Mix Swift and C++
  103. Model your schema with SwiftData
  104. Optimize CarPlay for vehicle systems
  105. Optimize app power and performance for spatial computing
  106. Rediscover Safari developer features
  107. Simplify distribution in Xcode and Xcode Cloud
  108. Take SwiftUI to the next dimension
  109. Update your app for watchOS 10
  110. Verify app dependencies with digital signatures
  111. What’s new in App Clips
  112. What’s new in App Store Connect
  113. What’s new in App Store pricing
  114. What’s new in App Store server APIs
  115. What’s new in AppKit
  116. What’s new in CSS
  117. What’s new in privacy
  118. What’s new in voice processing
  119. Work with Reality Composer Pro content in Xcode
  120. Your guide to Metal ray tracing
  121. Analyze hangs with Instruments
  122. Animate symbols in your app
  123. Beyond the basics of structured concurrency
  124. Build a multi-device workout app
  125. Build better document-based apps
  126. Build robust and resumable file transfers
  127. Build spatial SharePlay experiences
  128. Create 3D models for Quick Look spatial experiences
  129. Create a great spatial playback experience
  130. Create practical workflows in Xcode Cloud
  131. Demystify SwiftUI performance
  132. Design Shortcuts for Spotlight
  133. Design dynamic Live Activities
  134. Discover Quick Look for spatial computing
  135. Do more with Managed Apple IDs
  136. Explore media formats for the web
  137. Explore pie charts and interactivity in Swift Charts
  138. Explore rendering for spatial computing
  139. Extend Speech Synthesis with personal and custom voices
  140. Go beyond the window with SwiftUI
  141. Improve Core ML integration with async prediction
  142. Integrate with motorized iPhone stands using DockKit
  143. Meet Core Location Monitor
  144. Meet the App Store Server Library
  145. Migrate to SwiftData
  146. Optimize GPU renderers with Metal
  147. Optimize machine learning for Metal apps
  148. Protect your Mac app with environment constraints
  149. Prototype with Xcode Playgrounds
  150. Reduce network delays with L4S
  151. Support external cameras in your iPadOS app
  152. Update Live Activities with push notifications
  153. Use Core ML Tools for machine learning model compression
  154. What’s new in App Store pre-orders
  155. What’s new in Background Assets
  156. What’s new in Core Data
  157. What’s new in ScreenCaptureKit
  158. What’s new in Wallet and Apple Pay
  159. What’s new with text and text interactions
  160. Animate with springs
  161. Customize on-device speech recognition
  162. Design considerations for vision and motion
  163. Discover Metal for immersive apps
  164. Discover streamlined location updates
  165. Dive deeper into SwiftData
  166. Elevate your windowed app for spatial computing
  167. Embed the Photos Picker in your app
  168. Explore AirPlay with interstitials
  169. Explore testing in-app purchases
  170. Explore the USD ecosystem
  171. Keep up with the keyboard
  172. Meet Assistive Access
  173. Meet Swift OpenAPI Generator
  174. Meet device management for Apple Watch
  175. Share files with SharePlay
  176. The SwiftUI cookbook for focus
  177. What’s new in Core Motion
  178. What’s new in Safari extensions
  179. What’s new in Web Inspector

References

Read More...

Wednesday, May 17, 2023

Xcode 14.3 and Navigation Bar Issue

Recently, I encountered a very frustrating crash issue in my iOS app. After I updated my Xcode to version 14.3, my app suddenly crashed right at the launch time. I could not find where the crash occurred, even thought I set a break point at the very beginning of my project code.

I posted a question with my case on StackOverflow two days ago. That's the place where I normally try to get help from other talent developers around the world. The next day, I got two comments, which gave me direction on where to do further investigation.

Quickly, I found the place where the exception was raised and finally found a solution to resolve the issue! To cut a long story short, I put my question, my solution, and my findings about the issue in the hyperlink of my references.

References

Read More...

Saturday, September 03, 2022

Unselect cell in tableview if the cell is visible

I have an iOS app. The main screen has a tabbar on the bottom with three tab bar items. Here I'll explain two related tabs. One of them, the “export” tab, is for displaying objects in a tableview. It allows the user to make multiple selections. Another one, the "edit" tab, is for editing objects. If a user makes any changes to objects, any previously selected object items in the export tab will be cleared if the user switches back to the export tab.

Multiple Selection in Export View

In order to keep selected objects consistant, I created a class called SelcHelper to catch selected objects' indexPath. In ExportViewController, there is a function selectRow which is used to select or deselect a row in the table view.

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  private var selHelp[er = SelHelper()
  5.  private func selectRow(
  6.    select: Bool,
  7.    tableView: UITableView,
  8.    at indexPath: IndexPath)
  9.  {
  10.    if select {
  11.      tableView.selectRow(
  12.        at: indexPath,
  13.        animated: true,
  14.        scrollPosition: .none)
  15.      } else {
  16.        tableView.deselectRow(
  17.          at: indexPath,
  18.          animated: true)
  19.      }
  20.  }
  21.  ...
  22. }

Here is how I implement multiple selection in tableview events:

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  ...
  5.  func tableView(
  6.    _ tableView: UITableView,
  7.    didDeselectRowAt indexPath: IndexPath)
  8.  {
  9.    selHelper.remove(indexPath: indexPath)
  10.    tableView.reloadRows(at: [indexPath], with: .none)
  11.  }
  12.  ...
  13.  func tableView(
  14.    _ tableView: UITableView,
  15.    didSelectRowAt indexPath: IndexPath)
  16.  {
  17.    selHelper.add(indexPath: indexPath)
  18.    tableView.reloadRows(at: [indexPath], with: .none)
  19.  }
  20.  func tableView(
  21.    _ tableView: UITableView,
  22.    cellForRowAt indexPath: IndexPath)
  23.    -> UITableViewCell // cellForRow event
  24.  {
  25.    let cell = tableView.dequeueReusableCell(
  26.          withIdentifier: "cell")
  27.    ...
  28.    let isSelected =  selHelper.isIndexPathSelected(indexPath)
  29.    cell.isSelected = isSelected
  30.    selectRow(
  31.      select: isSelected,
  32.      tableView: tableView,
  33.      at: indexPath)
  34.    return cell
  35. }

The above codes work perfectly for multiple selections.

Users may edit objects in the "edit" tab view after they make some selections in the "export" view. As mentioned above, in the case of any objects changed, I want to clear all the selections in the "export" tab view. The following codes are added in the event of viewWillAppear to clear all the catched indexPath values.

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  override open func viewWillAppear(
  5.    _ animated: Bool)
  6.  {
  7.    super.viewWillAppear(animated)
  8.    ...
  9.    if myDBModel.dataChanged
  10.    {
  11.      selHelper.clear() // clear all indexPath
  12.    }
  13.  }
  14.  ...
  15. }

The logic is very clear. In the event of viewWillAppear, if the datasource has any changes, I'll clear all the catched selection indexPath values. This clearance will guarantee the cellForRow call to set cells unselected. Since I use a core data source for my export tableview, the cells with objects changed and in the visible area will be called to reload the cell.

Problem for visible cells

However I found a problem. If the visible cells in the tableView have any selections, the selection marks are gone. I verified that the cellForRow event was called after the event of viewWillAppear and all the selected cells are set to unselected. There is a problem: the cells are still in a high-light status. This status causes the cell to not be selected or unselected again!

For example, here is a cell is selected:

I changed the datasource first, and then back to the export tabview. The viewWillAppear event is called and all selections are set to deselected. The visible cell looks like this: the selection check mark is gone, but it looks like it is still highlighted and cannot be selected again.

I could scroll the tableView to move those visible cells to an un-visible area and scroll back. They are back to their normal status: unselected and not hilighted. However, what if there are only a small number of objects and they cannot be moved out of the tableView? The user would be stuck with this frastration situation and not be able to make a selection again. The user has to kill the app and restart the app to get back to normal.

Found a Soltion!

I think that this situation might be a problem or bug in UIKit. I have been struggling to find a way to resolve the issue for days. After several days and various attempts, suddenly, one evening very late, I found a solution to resolve the issue!

The solution is actually very simple. What I need to do is to call my private function selectRow to clear all selection for the visible cells in the event of viewWillDisappear! This clearance is OK. If there were no data changes, the cellForRow would not be called. If there is any data change, the cached selection indexPath values are cleared in the event of viewWillAppear first, and then cellForRow will be called to deselect visible cells.

  1. class ExportViewController:
  2.  BaseTableViewController // this is my base class on UITableViewController
  3. {
  4.  ...
  5.  override func viewWillDisappear(_ animated: Bool)
  6.  {
  7.    super.viewWillDisappear(animated)
  8.    guard let ips = tableViewInBase.indexPathsForSelectedRows
  9.      else { return }
  10.    // deselect all select rows in tableview on exit
  11.    // this will guaranty selection works again on
  12.    // appearance!
  13.    for ip in ips {
  14.      selectRow(select: false,
  15.        tableView: tableViewInBase,
  16.        at: ip)
  17.      }
  18.    }
  19.  ...
  20. }

References

Read More...

Friday, August 19, 2022

iOS Core Data Database Update

The Core Data database is the most commonly used as a data store for iOS apps. Nobody could design the database once and never change it again. I occasionally need to make some changes or update my Core Data database.

When the database scheme is changed, for example, adding a new entity, removing an entity, or changing entity fields, how could such kinds of changes affect your app? Would your app users lose all the data if any changes are made?

If you don't set up your database correctly, you may cause users frustration or pain. Fortunately, Apple provides a very solid and complete core data framework to allow you to migrate database changes during your app's life time without losing any data. In most cases, you would not need to do anything in particular. In some cases, you may need to write some codes to update data since you know what should be done.

Here are some notes regarding this issue.

Set the Correct Core Data Store Options

For the time being, in iOS 10.*, a Core Data store is created and managed by means of NSPersistentContainer. At the time this container is created, some critical options have to be set so that future updates or changes will be migrated smoothly without losing data.

The following is an example of how to set up the container with important options. If you don't set those, any changes to the database store would cause data loss because of a totally new schema.


  1. let storeDesription = NSPersistentStoreDescription(url: url)
  2. storeDesription.shouldInferMappingModelAutomatically = true
  3. storeDesription.shouldMigrateStoreAutomatically = true
  4. container.persistentStoreDescriptions = [storeDesription]
  5. container.loadPersistentStores() {
  6.    (storeDescription, error) in
  7.    if let error = error as NSError? {
  8.        // Handle errors here
  9.    }
  10. }

Add a New Version of the Database

In Xcode 9.4, if you need to change your database schema, don't make the change directly to the database model. Instead, add a new version to your database model and make the new version the active model.

From the menu of Editor->Add Model Version...,



Accepting the recommended model name, a new model version will be added.


In the right-hand inspector, add a new model identifier and select the new version as the current model version.

In this way, you can carry the previous database schema as-it-is to the next new version and make changes to the new version.

In most cases, all your data will be kept in the new database without any loss. Even though you have to test your app after your update to make sure it will keep or present previous database data smoothly.

Add Your Customized Updates

If you need to update your data as you know what the changes are and how to set up data correctly, you can do it at your app startup time.

For example, here is my practice of updating data at the app delegate class:

  1. @UIApplicationMain
  2. class AppDelegate : UIResponder, UIApplicationDelegate {
  3.  func application(_ application: UIApplication,
  4.    didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
  5.    {
  6.       // Override point for customization after application launch.
  7.       MyUserDefaults.shared.setupDefaults()
  8.       AppDatabase.shared.updateDatabase()
  9.       return true
  10.    }
  11. ...
  12. }

I use UserDefaults, i.e., app settings, as a place to save app and database versions and load those versions at the app's startup time. Then in the updateDatabase() method, I'll check if the previous app and database versions are older than a given version. If so, then some updates will be made so that the data will be kept up to date. Here is my example case:

  1. class AppDatabase {
  2.  static var shared: AppDatabase {
  3.    struct local {
  4.      static let instance = AppDatabase()
  5.    }
  6.    return local.instance
  7.  }
  8.  func updateDatabase() {
  9.     guard VersionManager.shared.isOlderThan(.database_v1_2_0) else { return }
  10.     cleanUpDataBase()      // do some cleanup missed in old db
  11.     updateSortProperties() // set sort values for the new fields
  12.  }
  13.  ...
  14. }

References


Read More...

Friday, August 12, 2022

Building a Bridge between Swift and JS

This was a challenge for me to make Swift talk to JS or JS to Swift. I have been working on an iOS app for years. With so many years of practice, I think I am very good at a deep technical level. I have also done a lot of html and JS programming. What I know about JS is mainly based on what I need to do on my blog.

In my app updates, what I need to do is to provide some information to users about how to use the app. An HTML view is a good choice for this purpose. However, I encountered a challenge that some of the information HTML needs is actually available easily on the Swift side. How can I let JS make a request to Swift, and then Swift pass the requested information back to JS?

This is what I mean by a bridge between Swift and JS.

Working Environment

In my project, I use a simple ViewController with a WKWebView UI component. The class is a very simple one. In the viewDidLoad event, an html file will be loaded into WKWebView. The content defined in the html file will be displayed on the view screen.

  1. class MyWebViewController: UIViewController
  2. {
  3.   @IBOutlet weak var webView: WKWebView!
  4.   {
  5.     didSet {
  6.       setup() // Place 1: magic will be injected from here.
  7.     }
  8.   }
  9.   var htmlUrl: URL? = nil
  10.   override func viewDidLoad()
  11.   {
  12.      super.viewDidLoad()
  13.      guard let url = htmlUrl else { return }
  14.      webView.loadFileURL(
  15.            url
  16.            allowingReadAccessTo: url)
  17.      let request = URLRequest(url: url)
  18.      webView.load(request)
  19.   }
  20.    override func viewWillDisappear(_ animated: Bool)
  21.    {
  22.        super.viewWillDisappear(animated)
  23.        // Place 2: some codes to be added here.
  24.    }
  25. }

My html file contains information about how JS is loaded. Normally, a JS script file is defined in the head section. For example,

  1. <head>
  2.   <meta charset='UTF-8'/>
  3.   <!-- JS functions used in html  -->
  4.   <script type="text/javascript" src="../myScriptFile.js"></script>
  5.   ...
  6. </head>

As you can see from the above structure, I have a view controller with a web viewer, in Swift code to present html content. The JS files which will be referenced from html are in my project bundle. This is my working environment.

A Case Study

Here is my case. In my html content, there is one element for displaying my app version.

<h2 class="center">Version <span id="appVersion">to be filed</span></h2>

This could be easily done by using a JS function like this:

  1. function swift2JSCallFor(appVersion) {
  2.    let e = document.getElementById("appVersion");
  3.    e.innerHTML = appVersion;
  4. }

The version string can be easily obtained in Swift code. How can I make a call from Swift to this JS function with version information?

WKScriptMessageHandler as a Gateway

Fortunately, Apple provides an API for the case of my view controller with WKWebView. WKScriptMessageHandler is the name of this API. This handler protocol can be implemented as a message channel gateway between Swift and JS.

This gateway could be implemented in my view controller class. There are some codes required to set it up. The disadvantage of doing this in my view controller is that I would have to repeat a block of similar code again and again in other view controller classes if I needed to do a similar thing. Another point is that adding some codes just for this kind of gateway would pollute my view controller class and make it harder to read and maintain.

Build My Gateway Class

After my thorough study and tests of this framework and a good understanding of what is required, I decided to create a new class to encapsulate the business logic or structure outline inside my class and expose the necessary settings for customizing.

  1. import WebKit
  2. typealias ScriptMessageHandlerBlock =
  3.    ((WKUserContentController,
  4.      WKScriptMessage) -> ())
  5. class MyWKScriptBridgeHelper: NSObject
  6. {
  7.    init(
  8.        observer: String,
  9.        scriptMessageHandlerBlock: ScriptMessageHandlerBlock?
  10.    )
  11.    {
  12.        webObserver = observer
  13.        wKScriptMessageHandlerBlock = scriptMessageHandlerBlock
  14.    }
  15.    private var webObserver: String = ""
  16.    private var wKScriptMessageHandlerBlock:
  17.    ScriptMessageHandlerBlock? = nil
  18.    var webObserverName: String {
  19.        return webObserver
  20.    }
  21. }
  22. extension WKScriptMessageHandler: WKScriptMessageHandler
  23. {
  24.    func userContentController(
  25.        _ userContentController: WKUserContentController,
  26.        didReceive message: WKScriptMessage)
  27.    {
  28.        guard let block = wKScriptMessageHandlerBlock else
  29.        {
  30.            return
  31.        }
  32.        block(userContentController, message)
  33.    }
  34. }

The class logic is simple. Only two parameters are required to create an instance of this class. One is a webObserver name, which will be called from JS to Swift to the gateway implemented in this class. Another one is a block of code where Swift information will be gethered or prepared and JS functions will be called.

Set it up on the Swift Side

OK, so far so good. It is time to put it all together in practice!

In my view controller class, there are two places where I will put some code. The first place is to set up my gateway instance of the MyWKScriptBridgeHelper class.

  1. class MyViewController
  2. {
  3.  private var scriptHelper: MyWKScriptBridgeHelper!
  4.    ...
  5.  private func setup()
  6.  {
  7.    let webObserver = "appVersion"
  8.    let block: ScriptMessageHandlerBlock
  9.        = {
  10.            [weak self] (userController, message) in
  11.            guard let this = self else {
  12.                return
  13.            }
  14.            // Make sure that it is html js function callback
  15.            guard message.name ==
  16.                    webObserver else {
  17.                return
  18.            }
  19.            // get app version string
  20.            let v = ... // get app version
  21.            let jsFunc = String(
  22.                format: "swift2JSCallFor('%@')",
  23.                v)
  24.            // Sends back app version to hml js function
  25.            this.webView.evaluateJavaScript(
  26.                jsFunc,
  27.                completionHandler: nil)
  28.        }
  29.        scriptHelper = MyWKScriptBridgeHelper(
  30.            observer: webObserver,
  31.            scriptMessageHandlerBlock: block)
  32.  }
  33.  ...
  34. }

In this setup(), two parameters are prepared for creating the instance of MyWKScriptBridgeHelper. One is the name of "webObserver" as the message name of the gateway. Another one is the block to verify the right message is received, to prepare the app version, and finally call a JS function "swift2JSCallFor()" to send the app version to the JS side.

The second location is in the view controller disappear event, where you should perform some cleaning by removing any message handlers and deallocating scriptHelper to avoid memory leaks.

  1. override func viewWillDisappear(_ animated: Bool)
  2. {
  3.   super.viewWillDisappear(animated)
  4.   guard let sh = scriptHelper else { return }
  5.   // Clear message handler from script helper
  6.   webView.configuration.userContentController.removeScriptMessageHandler(
  7.       forName: sh.webObserverName)
  8.   scriptHelper = nil
  9. }

That's all I need to do to set my gateway up. First, the gateway will handle a message requesting the app version from the JS side. Then it will call a JS function to pass the app version to JS.

HTML and JS Configuration

On the html and JS side, the first step is to make a request to Swift for the app version. This can be done by calling a JS function on html body element's onload attribute.

  1. <body onload="updateVersion()">
  2. ...
  3. <h2 class="center">Version
  4. <span id="appVersion">to be updated...</span></h2>
  5. ...
  6. </body>

The second step is to set up Javascript functions to make a request and to handle Swift calls.

  1. function updateVersion()
  2. {
  3.  try {
  4.    var msg = {}; // for empty parameter, it has to be {}
  5.    // "appVersion" is the request message name on Swift side
  6.    window.webkit.messageHandlers.appVersion.postMessage(msg);
  7.  } catch(err) {}
  8. }
  9. // function to be called from Swift to pass app version
  10. // to JS.
  11. function swift2JSCallFor(appVersion) {
  12.  let e = document.getElementById("appVersion");
  13.  e.innerHTML = appVersion;
  14. }

The comment description in the Javascript explains everything. That's all. With the above configuration, Swift and JS can talk to each other and pass the required information as needed.

Here is a snapshot of my html view with the correct version updated. I am so happy that this update will be automatically done and I don't need to touch the html file in future updates.

I have a short summary at the conclusion of this blog. Swift-JS communication was new to me. I spent more than one week investigating and doing my tests. The above strategy is based on other resources (see references) and my effort. This is not the best and complete solution. I may miss something. Please leave comments and provide your better solutions. Critics are welcome.

References

Read More...

Tuesday, August 02, 2022

HTML Head Settings

Recently, I have been busy working on my iOS updates. One feature I want to add is to provide some information views in the app.

There are many ways to present information on display in an app. I could use a rich-text component or a text view with attributed strings. However, compared to HTML, I prefer to use a web viewer to display HTML content. The most recent update and recommended strategy by Apple is WKWebView. This component fully supports HTML5 features with extensive Apple support for iOS devices, such as dynamic text size, zooming and pinch support, accessibility support, and more.

ViewController With WKWebView

With the HTML strategy, I could use HTML to format my information content with css styles, images, and js scripts. The way it works is just like a web server/client fully functional environment. My project acts like a web server. I place all my HTML files, images, css files, and js files into groups. Add a view controller class with WKWebView as the main container.

  1. class MyHTML1ViewController: UIViewController {
  2.   private var webView: WKWebView!
  3.   var htmlFile: String = "" // default value, can be changed
  4.   override func viewDidLoad() {
  5.    super.viewDidLoad()
  6.    let url = Bundle.main.url(
  7.            forResource: htmlFile,
  8.            withExtension: "html")
  9.    let request = URLRequest(url: url)
  10.    webViewBase.load(request)
  11.  }
  12. ...
  13. }

Then an html file can be loaded into the view, and the content is displayed in a way similar to a web browser.

Localization

In my project, Xcode provides support for localization. All HTML files, image files, and css files can be localised based on language support in my project. Currently, I have 3 languages to support, plus a base. A total of 4 sets of files are automatically created for me in my project. Taking an HTML file as an example, 4 HTML files are created for each language, and they are placed into a subfolder named by its language identifier. The same is true for images and css files.

Here is a snapshot of my project for groups of HTML-supported files. I group them into js, css, images, and html group names.

As you can see, each localizable item has a list of supported languages. With this visual layout, I can easily pick a language file and do my translation work.

The file structure is very different from the visual view of the project. Actually, all files are in the same root folder of projects. It is really hard to find them if I want to open Finder to find a file. That's why I named all HTML-supported files with a prefix of html_.

Further investigation, I found that within my app main bundle, all HTML supported localizable files within the project are placed in subfolders by the name of language identifiers. In other words, in my app bundle, all localised files are placed in a subfolder named as the language identifier. For example, for English, "en.lproj" subfolder contains all localised files, including my HTML-supported files. One level up from language subfolders is the root path, where all none-localized files sit. For example, js files are at the root level.

As I present all this in the above pictures, I will try to explain my understanding of how web server/client works within my app project. This picture of file layout is the web server's path structure. When an html file is loaded into WKWebViewer, all related images and css files are in the same path as the html file. Up one level is the place where js files are located.

With this understanding, I can edit my html files to refer to other source files and components easily. WKWebView is something like a browser to present HTML content and let users review and interact with it. As a client, the viewer has knowledge about where to get those sources and components from my project. I call it a server.

Keep in mind, I integrated WKWebView views into my project is based on HTML support and functionality. However, my app, built by my project, does not provide any end points for external web clients to access. My app is a native iOS app running on an iOS device.

Duplication of Settings in the head Section

I have several HMTL files to be presented by a number of view controllers with WKWebView, currently about, information and setttings. There will be expanded to more, let's say n HTML contents. Four language sets are supported in my project, and might be increased to more, m languages supported. You can imagine that n x m HTML files are in my project.

One thing I realise is that within an HTML file, in the document.head section, there are many settings. For example, common and specific css source files, common and specific js libray files, and meta configuration for various settings such as view point. Some of them are the same or repeated in all HTML files.

I realised there would be a maintenance issue. If any of these common parts are changed, as I will find some interesting feature support, I have to edit all those HTML files to do just copy and paste work. Is there any better way to do the maintenance job? Actually, I had encountered some issues where I forgot to update some languages, and those language displays did not work in the way I expected.

Seeking Help from SO

StackOverflow is a community of IT programmers' participation network. There are many talented people there to support others with all kinds of questions. After struggling to find a solution, I posted my question there. I was hoping to get advice from talented people.

Before I posted my Q, I tried various ways, including using Swift within my project. There is one way to open an HMTL url from my project. Then I could do parameter replacement of the content of the HTML file as a string. However, I remembered that I tried this method before. I just could not change any files within my application bundle!

The short story is that I tried to use a plist file within my project to save some setting changes. No errors are thrown out when some new changes are saved to the plist file. However, when the settings were read out later, they were back to their original values. I had been struggling for days to find out why, and finally I found that the plist file in the project bondle cannot be changed!

I resolved the issue by copying the plist file to the application sandbox supported folder. The plist file can be edited and saved there. After that, I come to my explanation: it may be a security reason that files within the app bundle cannot be changed.

I could copy the HTML file to outside of the bondle path and make changes there. What about other HTML sources and library files? It just defeats the language support naturally provided by the iOS framework if I have to find out or identify which language is supported, and then copy all those files from specific language subfolders out.

I knew there is another way to load an HTML string directly into WKWebView. The string can be changed with a parameter replacement. This would display HTML content. However, the same issue comes back to me again. How about linking other sources and library files if I load the view with a string? No path information from the string

Since I am using HTML technology, naturally I should find a way in HTML or js to inject common head settings into my loading HTML file.

Partial Working Solution

Within about a day of my post, I got two answers. One is using js to inject common settings from a js function. I tried it immediately. It did not work at first.

When I went back to my Q, my Q was marked as [losed] by a senior member of SO. The reason is that my working environment was not explained clearly. More explanation is required.

With [losed] status, no more answers are allowed. This actually turns many people away. The only way I could ask for help and get attention was to update my Q and add comments. As you can see, I have done several updates, and the Q is quite lengthy. Anyway, I think I have done enough to clarify my Q on SO.

If you read my Q, you will see a solution in it. Based on the only post's answer with codes, I made some changes, and took the meta setting for charset out of my common settings. It works now in my project, just as I expected, to inject the common settings into the head section.

Even though my solution is working, I still have questions about it, and I would like to see if there is any other better solution or advice on this issue.

Since SO is a member participation and support community, I need to get some people's help to turn off the [close] status. What is required is to have 2 more requests at the link of reopen, at the end of my Q.

References

Read More...