Developing the Profile Screen for Healthcare iOS App
Focused today on my healthcare-focused iOS app. đĽđą Developed the UI screen for the Profile section, enhancing user interaction and functionality. Excited to continue building and improving the app! đŞâ¨ #iOSDev #HealthcareApp #SwiftUI #UIModule #DevProgress #MobileDevelopment
Let me know if youâd like any further changes!
The following article shows how to build complex layouts in SwiftUI using grids:
https://bugfender.com/blog/swiftui-grid/
Implementing #freemium features should be easy.
That's why we added the `freeIf` parameter:
⨠One line of code
đ Automatic state handling
đŻ Perfect for usage limits
đ¨ Native #SwiftUI feel
Here's how simple it is: đ
#iOSDev #Swift
#SwiftUI #a11y Techniques app updated with new Horizontal Scroll Views good/bad example that applies WCAG's Reflow success criterion to avoid horizontal scrolling at 320 or less screen width and adds right and left arrow buttons as single tap alternatives. https://apps.apple.com/app/accessibility-techniques/id6474141089
Reorderable Favourites is one of those small features Iâve wanted for ages but kept getting pushed back for bigger things. Like most things in #SwiftUI Drag & Drop is great when it works and cursed when it doesnât. Very much hoping it works smoothly for everyone!
Today was interesting: got a bunch of nothing done, but learned a lot about how modules in JS work, and will probably refactor how JS is loaded site-wide as a result haha. Tomorrow, let's do that. See you then!
đ Tomorrowâs stream: https://youtube.com/live/Hslp-RVp9qQ
âŽď¸ Playlist so far: https://www.youtube.com/playlist?list=PLRxjf93xotuofCtaxtGOcWeuxVZYJyY-m
đ˛ Download Jiiiii: https://apps.apple.com/app/apple-store/id6472801548?pt=14724&ct=MastodonCCStreams&mt=8
#Jiiiii #DevStream #tvOS #visionOS #macOS #iOS #iPadOS #Anime #Swift #SwiftUI #Vapor #WebAuthn #BuildInPublic #TestFlight #PWA
I'm making a seasonal anime guide app, in the open for all to experience and learn from!
Let's flesh out the merge page to make progress there.
#Jiiiii #DevStream #tvOS #visionOS #macOS #iOS #iPadOS #Anime #Swift #SwiftUI #Vapor #WebAuthn #BuildInPublic #TestFlight #PWA
Come chill with me: https://youtube.com/live/F784ffsitH8
Headâs up: this post is a technical deep dive into the code of DocC, the Swift language documentation system. Not that my content doesnât tend to be heavily technical, but this goes even further than usual.
The Setup
While I was working on some documentation for the snippets feature in DocC, I ran into an issue with the mechanism to preview documentation. As soon as I added a snippet to an example project, the documentation would fail to preview about half the time. The command I use is:
swift package --disable-sandbox preview-documentation --target MyTarget
When I first started debugging this, I wasnât sure what caused the issue. I opened a bug in swift-docc-plugin (spoiler: the bug wasnât in swift-docc-plugin), thinking at first that it was always failing. As it turns out, it wasnât always failing â my luck was just poor, and the issue intermittent. I had several commits in one of my side projects that added snippets, which I used to work through my documentation of the feature. In order to write up the issue with reasonable reproduction steps, I created a series of commands to verify the behavior I saw. The flow is pretty simple:
At this point, I didnât realize that the issue was intermittent, so I iterated back and forth between commits, cleaning the .build
directory to see if that made a difference, and then ultimately noticed a change in behavior. At one point where I expected it to fail, it worked. Ah, glorious: A heisenbug. At least now I knew that Iâd have to repeat process multiple times to get the issue to show. With that in mind, I was able to nail down the change in my project that started to illustrate the issue â it was when I added the first snippet.
Thereâs another project (the exemplar, really) that hosts snippets in its documentation â swift-markdown â that *never* exhibited this problem. That was a real head scratcher. But I did have a reliable reproduction, so I focused on that.
When I work on an intermittent bug, I try to get a debugger attached on the code thatâs behaving badly. Because this was invoked through a SwiftPM plugin, I had my work cut out for me. Command plugins are separate executables that Swift package manager invokes internally. It is obnoxious to get a debugger attached to it. You canât easily do it directly from within Xcode, because Xcode isnât launching the executable. Thereâs a conversation about how to wrangle debugging a SwiftPM plugin on the Swift Forums that covers some of this. The way I resolved it this time is to put in a long sleep() command in the code of the plugin, run it through SwiftPM, use the terminal to hunt down the process ID that SwiftPM invoked, and attach the debugger to that ID. This is kind of a nasty manual process, so I used sleep(30)
â Iâm just not that fast at wrangling all the tools for this. I managed to get attached⌠and then realized I didnât need to.
While I was looking at the process list through the terminal to get the process ID, I spotted that the process in question (the plugin) was invoking yet ANOTHER process in turn. I actually knew this previously, and just plain forgot. The swift package preview-documentation
command is a light wrapper around doccâs preview
command. While I wasted some time with the plugin, this made debugging significantly less painful. I could invoke an example using the docc
binary directly. And yeah, it moved the target for what had the bug â it wasnât in swift-docc-plugin.
Debugging preview in DocC
I closed the issue in the plugin and opened a new issue in swift-docc, summarizing what Iâd learned and how to reproduce the issue. It was the end of a day when I got to this point, so I left things alone and came back the next morning. When I opened the issue, I verified the issue using the version of docc
released with the Swift toolchain. In my case â that meant the version included in the toolchain that ships with Xcode 16.1.
When I jumped back in, I had a the intent to verify the same issue exhibits with the latest code â against the main
branch. The issue request form asked for any issues to be verified against main
in order to verify that it hasnât already been resolved. There were also some comments in the issue â David referenced some other work pending that resolved some flaky tests, that â at a guess â might have an impact. So I buckled down to use the latest development branch of docc and repeat the process to verify the issue.
One of the quirks of verifying this issue is that docc
is a separate project from the javascript single-page browser app (swift-docc-render) that displays the content. When youâre running docc from the main
branch, it doesnât know where that content lives â you need to tell it. Fortunately, thatâs pretty easy. You set a specific environment variable and docc uses that to know where to load the content.
With that in place, and the example process invocation from my debugging the prior day, I had a way to run this directly. In the terminal, it looks something like:
export DOCC_HTML_DIR=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/share/docc/render//Users/heckj/src/swift-project/swift-docc/.build/debug/docc preview \/Users/heckj/src/Voxels/Sources/Voxels/Documentation.docc \--emit-lmdb-index \--fallback-display-name Voxels \--fallback-bundle-identifier Voxels \--additional-symbol-graph-dir /Users/heckj/src/Voxels/.build/plugins/Swift-DocC\ Preview/outputs/.build/symbol-graphs/unified-symbol-graphs/Voxels-7 \--output-path /Users/heckj/src/Voxels/.build/plugins/Swift-DocC\ Preview/outputs/Voxels.doccarchive
I prefer to use Xcode when debugging and fortunately thatâs not too hard to arrange. In order to set it up, I opened the Package.swift file of the docc
project with Xcode. It sets up the targets for you, and with a package like this, tends to default to the package target. I shifted the platform I was building for to âMy Macâ, and the target to the docc
executable. With those set, I opened âedit schemeâ for that combination.
Xcode lets you set environment variables and pass arguments to the executable when you invoke ârunâ. Thatâs perfect for what I was doing â working to easily reproduce the issue where I could debug it.
The scheme editor in Xcode for the run mode, that shows arguments set and an environment variable to make it easier to debug docc.I set the DOCC_HTML_DIR
environment variable and set up the arguments from my example. One thing I had not caught when I first did this was that the path in one of the arguments included a space. Once I realized there was space, and run wasnât working, I added a /
character to escape the space in the name (within âSwift-DocC Preview/outputs/â). With that in place, I was able to run the code and see the results, as well as run the debugger. The issue was, indeed, repeating itself with the main
branch.
Once I had that set up, checking the pull request that David mentioned in the issue was a piece of cake. Iâve had a poor time with Xcode handling changing a git branch underneath it, so I closed Xcode and updated the branch, using the gh
executable as a helper. When you look at a pull request on GitHub, the CODE button provides you with a command line snippet that you can copy and paste to get it on your local machine. In this case:
gh pr checkout 1070
Once it was checked out, I re-opened Xcode, and was just a matter of running a few more times. Fortunately, scheme settings that Xcode uses when you tweak run arguments arenât generally overwritten when you switch to another branch. I had some hopes this might solve the issue, but they were dashed pretty quickly. With 5 runs, I was able to verify that the code update didnât make a difference in my example.
Verifying beyond swift-docc-plugin
Since I didnât get the quick win with the pull request, it was time to dig further. Switching back to the main
branch, I took it from the top. I start by looking at how code gets executed within docc
.
The entrance point for docc (https://github.com/swiftlang/swift-docc/blob/main/Sources/docc/main.swift) very quickly leads to a swift argument parser setup (https://github.com/swiftlang/swift-docc/blob/main/Sources/SwiftDocCUtilities/Docc.swift). It is quick to see its broken up into subcommands, one of which is preview
. Finding the code for that subcommand is less than obvious just scanning at the folders and files, but command-clicking in Xcode gets you right there: https://github.com/swiftlang/swift-docc/blob/main/Sources/SwiftDocCUtilities/ArgumentParsing/Subcommands/Preview.swift. The preview subcommand code, in turn, uses a PreviewAction that has a perform()
function where the âwork gets doneâ. The gist of which is:
When I first ran into this issue, what I saw from invoking a preview was an output that was missing the name of the module when it displayed the preview. I was reasonably familiar with what the contents of that directory should look like, so I ran it multiple times and captured a copy of the directory structure that it built when it worked, and another when it didnât. Comparing the two, the difference was that the top-level module for my example project just wasnât appearing. The directories included all the files for the symbols in my module, just not the module itself.
With that knowledge in hand, when I got to this âconvert first, then displayâ setup, I knew the path to search down into was the convert action. I also knew that it had something to do with the top-level module, since all the symbols were there â it was missing the top level module in the output.
Spelunking into convert
If I had to pick a âheart of DocCâ, it would be this conversion process.
The high-level workflow takes in two different kinds of data â one or more symbol graphs and a documentation catalog â and assembles a documentation archive from them. The result isnât plain files with HTML inside them. Instead itâs a collection of the data that represent those symbols (in JSON) that can be rendered â into HTML, or really any other target. The rendering happens with a different bit of code (thatâs the swift-docc-render project that I mentioned).
Symbol graphs are generated by the compiler (or other code, really â but generally by the compiler). But symbol graphs along donât have all the details in a way thatâs easy to collect and render them. The relationships between symbols, the type of symbol, and so on, gets cleaned and re-arranged in the convert process. It also mixes in the writing and resources that you provide in the docc catalog. This lets DocC override or add in content, as well as the provide things that donât exist in the raw code symbols, such as articles and tutorials.
The code in ConvertAction is fairly complex as thereâs a bit of abstraction there that makes it a little harder to parse. It abstracts the producer of data and the consumer, and has additional bits to support tracking documentation coverage, captures diagnostics for issues mixing the files together so they can be played back to tooling, and other options, such as building an index. All this is encapsulated in the _perform method. That method, in turn, runs this bit of code:
conversionProblems = try ConvertActionConverter.convert( bundle: bundle, context: context, outputConsumer: outputConsumer, sourceRepository: sourceRepository, emitDigest: emitDigest, documentationCoverageOptions: documentationCoverageOptions)
While ConvertActionConverter is a jump around the project code, itâs encapsulated pretty well. Itâs fairly straight forward to read and understand whatâs happening. Thereâs an inner function with a lot of comments in the flow of that method that made it harder to for me to track what was happening, and where the function boundaries were. Once I realized it what it was, I read around it to again look for that âwhereâs the core work happeningâ.
The heart of the convert function is:
let entity = try context.entity(with: identifier)guard let renderNode = converter.renderNode(for: entity) else { // No render node was produced for this entity, so just skip it. return} try outputConsumer.consume(renderNode: renderNode)
This code is wrapped inside a function context.knownPages.concurrentPerform
that iterates through the known pages. I wasnât sure where it might be dropping the top-level module, so I started with good old fashioned printf debugging. That also exposed a bunch of new types to explore and learn about:
knownPages
property on the context instance is a list of ResolvedTopicReference, which is tucked away in SwiftDocC/Model/Identifier.swift.entity
thatâs returned from context.entity
is an instance of DocumentationNode.I started off with a breakpoint on the bit of code that sets the entity from the identifier (the ResolvedTopicReference
). Pretty quickly I realized there were a LOT of these, even in my smaller sample project, and stepping through each iteration was kind of horrible. To work around this, I reverted to a variation on printf debugging. I started adding in code to see what was happening, and more specifically to look for what I was after â the node in the end result that represented the top-level module. My first printf debugging worked on printing the name (a String) of the entity.
if entity.name.plainText == "Voxels" { print("FOUND IT!: \(entity.name)")}
The first run through just printed them all â and generated something just over 500 lines, so I took a bit of time and looked through them all. Sure enough, somewhere in the middle of that list (theyâre not processed in alphabetical order) was the top-level module name that I was looking for.
I started tracking how and where that got created, and its properties set. I expanded my exploration code to only track what was in knownPages
to see what it was providing.
for page in context.knownPages { //print(page.absoluteString) if page.url.absoluteString == "doc://Voxels/documentation/Voxels" { print("PING") }}
The knownPages
is a computed property, filtering whatâs stored in the context:
public var knownPages: [ResolvedTopicReference] { return topicGraph.nodes.values .filter { !$0.isVirtual && $0.kind.isPage } .map { $0.reference }}
What I didnât track at the time, and found a bit later, was that the filter
statement turned out to be important. I didnât fully understand the details of what made up a âresolved topic nodeâ, and didnât know what it meant to be âvirtualâ or not. While kind
being page
was fairly obvious, virtual can have a lot of meanings and implications.
In either case, when it was working correctly, the known pages included the url, and when it wasnât, the page didnât appear to exist. Because of that, I knew the issue was somewhere in the execution flow prior to where I was looking. I read back to see where things get set up and initialized.
The convert action sets up the context with the following code:
let context = try DocumentationContext(bundle: bundle, dataProvider: dataProvider, diagnosticEngine: diagnosticEngine, configuration: configuration)
The DocumentationContext initializer is pretty lean, deferring the more complex setup to an internal register function. I continued to trace that back further, and the register function uses and ultimately references another type: SymbolGraphLoader.
I was repeating my printf debugging by dropping in that a of code that looked for the URL I wanted to find earlier and earlier, making sure it was there (or not) as I went. As I was getting to the registerSymbols function on DocumentationContext
, I realized the data types didnât include the URL. I needed to understand what was beneath. I started off looking for name
on the underlying types, and quickly found a surprise â it was always there, even when the URL wasnât.
Thatâs when I clued in that filter
added on knownPages
. I realized the key difference wasnât if the node existed, but how it was set up. The node always existed, and when the code was working, the isVirtual
property was false
. When it failed, isVirtual
was true
.
What the hell is isVirtual, whatâs it mean, and where does it come from?
I was a bit confused and frustrated at this point. Not that I couldnât find a comment that spelled out that âisVirtualâ meant that it shouldnât be rendered, but that I just didnât understand the implications and where it all came from, what it meant across all those contexts, and why it was needed. Turns out I didnât really need to know all that detail, but since it was what was different, I wanted to understand.
I took a bit of time to look at the raw JSON of a symbolgraph file, and found that isVirtual
comes from the compiler itself, and is carried through, for most symbol nodes. In that same process, I also realized that the symbol graph from the compiler did not have a symbol for the module itself. So something in the code that was loading the symbol graphs was adding a node for the module, and setting its value, and sometimes incorrectly. I continued to have this hypothesis that it was some wacky race condition in the code that hadnât been spotted. And it sort of was, but at the algorithm level, not the code-threading level.
As a side note,
isVirtual
in a documentation node fundamentally means âdonât render thisâ. The idea being that itâs only there to link things together â relationships, overlays, etc.
SymbolGraphLoader and SymbolKit
The symbol graph loader takes in one or more symbol graph files, merges them all together, cleans them up a smidge, and creates the nodes needed to represent the higher level connections and relationships. While I still didnât fully grok the isVirtual
property and its implications, I knew that how it was being set for the module level node was what I cared about.
When I was looking for that code, I found the following:
private static func moduleNameFor(_ symbolGraph: SymbolGraph, at url: URL) -> (String, Bool)
I hadnât yet joined the dots to see where it was set up, but knowing if something was a âmainâ symbol graph or not sounded promising. I kept that in mind while I dug further. I kept digging and found where the loader was collecting the symbol graphs, annotating them, and merging them. It SymbolGraph loader fed them all into an instance of GraphCollector. And that code is from a different library: SymbolKit.
Scanning through that code, I found the same function: moduleNameFor
. Same parameters and outputs, a public symbol in SymbolKit and private in Docc. Iâm guessing it started in Swift Docc, and was later extracted out into the library. The end result was identical logic in two places, so I made a note to clean that up later.
The GraphCollector
turned out to be key. It holds the source for the data thatâs used to determine the top-level module node.
The mergeSymbolGraph method in the graph collector pulls everything together. Within the collector, the data about the graphs are stored in dictionaries keyed by the name of the module. In addition to providing a unified graph by the module name, it also keeps track of each modules it loads, and marks it as a primary module or an extension module. The âis it the primary module graphâ setting uses the logic in moduleNameFor
.
In this function, you provide a loaded symbol graph and the name of the url, and it returns back the name of the module described in the graph and if it is a primary module. The key logic that makes this determination was the following line:
let isMainSymbolGraph = !url.lastPathComponent.contains("@")
The presumption when this was written was that all symbol graphs would come from the compiler, and the ones that extend an existing symbol graph will have an @
symbol in the name. Snippets blow up the assumption. The snippet-extractor code, that creates the symbol graph from snippets, names the symbol graph file YourModule-snippets.symbols.json
. Because the name didnât include an @
symbol, snippet graph files were being regarded as âprimaryâ graphs.
Back in the DocumentationContext, thereâs an extension on SymbolGraphLoader that provides the URL for the module: mainModuleURL â and this is where the flaw exhibits. The extensionâs method uses first
on the list of graphLocations from the collector to get the primary module, which assumes thereâs only one. When more than one exists, it returns a non-deterministic result. Sometimes it was returning the one referenced from the snippet symbol graph, and other times it was the ârightâ symbol graph.
Itâs not a direct path to easily find where and how thatâs used to set up the URL. The line of code that pulls this detail:
let fileURL = symbolGraphLoader.mainModuleURL(forModule: moduleName)
uses that later to mix together the topic graph:
addSymbolsToTopicGraph(symbolGraph: unifiedSymbolGraph, url: fileURL, symbolReferences: symbolReferences, moduleReference: moduleReference)
This is what ultimately mixes in the isVirtual
property into the topic graph, and thatâs what sets the module to isVirtual â assuming thereâs only one and using the âfirstâ one it grabs.
Fixing the issue
The work above took place over the course of 3 days and resulted in 3 issues reported, one each to swift-docc-plugin, swift-docc, and swift-docc-symbolkit. The first of those I closed as soon as I realized it had nothing to do with the issue.
I stripped back out my printf debugging code, and in the end there were two relatively small changes that I made into pull requests â one for the fix in SymbolKit, and a supplement that just cleaned things up a bit in DocC.
Iâve proposed a solution that changes the logic in SymbolKit to support the fundamental assumptions that âthere can be only oneâ main symbol graph. I did think about trying to represent the snippets as a different type in the collector (other than primary and extension), but I spotted a number of other places in the code that had that idea heavily built-in. They also leveraged .first()
to get at the primary module, if it existed. Since snippets were added a couple of years ago, and this hadnât been identified and debugged, I wasnât sure what was expected for the function returning the name and processing of snippet symbol graphs. I opted for a change that tweaks the logic in SymbolKit, adding an inspecting of the isVirtual
property in the metadata of the module in the symbolgraph in addition to the verifying there wasnât an @
symbol in the filename.
I also opened a supplemental PR in docc to de-duplicate that logic and keep it all in once place. The main issue (the comments in which are a summarized, short-form of this post) looks like itâll be fully resolved by the change in SymbolKit. But since Iâd done the digging, and noticed the duplication, I figured it wouldnât hurt to help clean things up a bit.
https://rhonabwy.com/2024/11/07/code-spelunking-in-docc/
#apple #docc #ios #swift #swiftui #technology
I'm curious how #SwiftUI folks organize their #Firebase CRUD functions. It seems like I can create these as static functions inside a class, then reference the class.function whenever I want to invoke them. Any concerns with this approach? Any good examples using recent SwiftUI techniques if other method are preferred? Thanks!
I was following along with @peterfriese's Build a Second Brain w/Firebase & #SwiftUI (thx, Peter!) and noticed he had some sort of Loren Ipsum generator used (around 18:10). I've not seen this before. What is it & how do I get it in Xcode? Thx!
Updated the #SwiftUI Sliders #a11y demo to meet #WCAG's Pointer Gestures success criterion that requires single tap alternatives to the gesture used to adjust the slider.
https://apps.apple.com/app/accessibility-techniques/id6474141089
Hi fellow Apple devs! How is Xcode 16 these days? Latest beta better?