When Apple introduced SwiftUI back in July we immediately knew it was going to generate a lot of expectations. As app developers ourselves, we are very aware about the complexity of User Interface development in iOS.
UI has been keeping apps especially expensive and error prone along the years. Many frameworks were created to improve this situation like ComponentKit, Texture or even React Native. However, using one of those frameworks felt like getting married: once you are in, you are in, even if you don’t like some features.
Using UI frameworks that abstract you from UIKit creates an enormous dependency on the said framework evolution. On the other side, using a framework only for some parts of the app leads to terrible amounts of boilerplate code. As a result, iOS developers have traditionally been very cautious about exploring the world beyond UIKit.
How is SwiftUI different from all those other UI frameworks?
Well, it’s created by Apple. You can invest your time learning SwiftUI with the security that is not going to be abandoned or become incompatible with a future iOS version.
And what has Bugfender to say about SwiftUI?
At Bugfender we like playing with beta versions, but only at home. It’s important to understand that the BugfenderSDK is installed in millions of devices around the world and sending logs from almost each and every country on the planet. This is a huge and heavy responsibility to carry and that’s why we always wait until everything is stable before starting to experiment.
However…
During last weeks we got A LOT of requests from developers to make Bugfender compatible with SwiftUI and with Project Catalyst.
The community was really expecting some UI movement from Apple and many developers want to use SwiftUI in production as soon as possible.
On the other side, many iPad apps developers want to port their apps to take advantage of the first wave of “UIKit for Mac” apps.
Message received!
After receiving a few messages in our Intercom and feature requests in our GitHub we knew we had to do something about SwiftUI and Project Catalyst. And well, honoring the truth, the iOS team was thrilled to get an excuse to experiment with them ✌️.
SwiftUI and Bugfender
Bugfender automatically logs UI events in iOS and Android. This is one of the most loved features of our platform because developers can understand the exact flow that lead their users to a bug.
It requires just a line of code:
Bugfender.enableUIEventLogging()
Our first goal was to port this feature to SwiftUI.
It didn’t take us too much to conclude that this was not possible at the moment. The SwiftUI API is composed of immutable classes that implement the View protocol.
Every attempt we could think of to iterate along the SwiftUI hierarchy (like playing with generics or using the Mirror API) failed at some point.
After many hours of experiments we decided to reduce our goals to “best effort” and we created a helper class that could ease the Bugfender usage for now.
Funny thing, just the day after finishing this helper class Apple updated Xcode and changed the SwiftUI API making our code deprecated (d’oh!).
Actually many of the articles and tutorials you will find on the web are already deprecated and will not compile. This is expected when you work with betas, no complains about it. A different thing is the lack of documentation, that felt a bit frustrating.
We won’t be officially supporting SwiftUI until we have a wider view of the framework backed by official Apple docs. But, now that iOS 13 has been released we have decided to share a few tips to use Bugfender with SwiftUI.
Get notified when a view is presented on screen
Until now, if you enabled the Bugfender UI logging, you would get logs from UIKit when a UIViewController
was presented on screen or when a control was used (a button pressed or the value of a switch changed, for example).
A SwiftUI View has to be contained in a UIHostingController
, a base class from UIViewController
.
When this Hosting View Controller is presented, the log will still be sent to Bugfender, however Bugfender has no access to what happens inside the SwiftUI view.
You will have to manually setup the logs in the onAppear
method of the Views you are interested like this:
struct ContentView: View { var body: some View { ImportantView().onAppear { BFLog("Important view was shown") } } }
But instead of modifying the onAppear
for each view your are interested in – as is done in the above block – you might ease things a bit writing an extension:
extension View { func bflog() -> some View { // Add an onAppear modifier to the View return self.onAppear { let className = String(reflecting: self) BFLog("onAppear - \(className)") } } }
This extension adds an action to the view that it’s executed when the view appears.
Now you can just write:
struct ContentView : View { var body: some View { // This view will notify BF on appear ImportantView().bflog() } }
Notice that this adds and action but doesn’t override the original onAppear
method. You are still free to append another onAppear
and it will be executed as expected
struct ContentView : View { var body: some View { ImportantView() .bflog() .onAppear { print("This will be printed before logging to Bugfender") } } }
Remember to use bflog()
as the first modifier otherwise, if you append it after another modifiers (like foregroundColor
, shadow
, …) then String(describing: self)
will return a long string containing the full tree of View
s and modifiers applied to that chain. This is a feature that we really loved as it might help us to do great things in the future (like exporting the full view tree) but this is another history.
The extra mile
You can alternatively add another extension to onDisappear
to be notified about a view dismissing or you can get creative and pass a parameter with a convenient log that gives you more information.
However, a really nice thing you can do now is to use a tag in your log to ensure it will really show up in the console of Bugfender as a real UI log:
extension View { func bflog() -> some View { return self.onAppear { let className = String(describing: self) Bugfender.log(lineNumber: #line, method: "onAppear", file: "", level: BFLogLevel.default, tag: "UI", message: className) } } }
Log a button when is pressed
In the case of a Button our problem is slightly different.
Let’s check it with an example:
struct ContentView : View { var body: some View { Button(action: { BFLog("The button was pressed") doSomethingUseful() }) { Text("Press here") } } }
The callback of a Button is now a parameter in the constructor of the Button. As you might imagine, action is a private property. There is no chance that we can access and modify it.
You will have to add your Bugfender calls in the Button callback constructor. But again, the work can be a bit simplified. This time you can create a wrapper class around Button like this one:
struct BFButton<Label>: View where Label: View { var action: ()->Void var label: Label public init(action: @escaping ()->Void, @ViewBuilder label: () -> Label) { self.action = action self.label = label() } public var body: some View { Button(action: { BFLog("Button pressed") self.action() }) { label } } }
Basically, what we are doing here is wrapping Button so we can log to Bugfender before calling the users action.
Now, we will be notified when a button is pressed just changing the class:
struct ContentView : View { var body: some View { BFButton(action: { doSomethingUseful() }) { Text("Press here") } } }
If you just use this code as is, you will get a lot of “Button pressed” logs but if you have more than one Button
in your application – and it is expected that you will – you will be unable to identify to which button are referring the logs.
The easiest way to identify the button is to add a name to the log.
In the “old” UIButton
this was pretty easy because it was safe to call button.title
. In SwiftUI a button also has a label. However this label is defined as some View and it might be literally anything. We can not use this content to identify the concrete button. We will need to force ourselves to add a name to the button in the constructor.
In parallel, we can also go the extra mile and simulate the same behavior of Bugfender in UIKit so we get the logs well formatted in the console of Bugfender.
struct BFButton<Label>: View where Label: View { var name: String var action: ()->Void var label: Label public init(name:String, action: @escaping ()->Void, @ViewBuilder label: () -> Label) { self.name = name self.action = action self.label = label() } public var body: some View { Button(action: { let message = "\(self.name) button pressed" Bugfender.log( lineNumber: #line, method: "", file: "", level: BFLogLevel.default, tag: "Interaction", message: className) // Call to the original action self.action() }) { label } } }
Project Catalyst and Bugfender
Similar to what happened with SwiftUI Apple has released a very limited amount of information about Project Catalyst.
“You just click in the Mac checkbox and the app will compile for Mac” they said.
Well… Not exactly.
The Bugfender SDK includes the iPhoneOS and the iPhoneSimulator slices so you can build your app for simulator and for an iOS device using the same framework file.
When Project Catalyst was announced our first thought was that we needed to add a MacOS slice to the framework. But this is not how it works. Apple has introduced a new architecture which is called UIKit for Mac. Once we include this new slice BugfenderSDK should be running on your UIKit for Mac app.
We’re facing some issues doing that with the Xcode, we started with different betas on our first (and second, and third) attempt but now that the final iOS 13 SDK has been released you should expect it soon.
That’s all from Bugender today. If you think we can improve our examples somehow or you face any problem trying to use them just reach our engineers via Facebook or Twitter.