diff --git a/2014/07/25/objective-c-prefixes-a-thing-of-the-past/index.html b/2014/07/25/objective-c-prefixes-a-thing-of-the-past/index.html new file mode 100644 index 0000000..89f272a --- /dev/null +++ b/2014/07/25/objective-c-prefixes-a-thing-of-the-past/index.html @@ -0,0 +1,62 @@ + + + + Objective-C prefixes: a thing of the past? - Scott Berrevoets + + + + + + + + + + + + +
+
+

Objective-C prefixes: a thing of the past?

+ + +
+
+

This past week, there was, again, a lot to do about Objective-C and prefixing. To most iOS developers, the story sounds familiar: one camp is strongly in favor, another camps is strongly against, and a third camp couldn't really care less.

+

Unless I'm unaware of any other platforms where the discussion was also a hot topic, this time I was at least a little responsible for instigating a debate that will never see a clear winner, with the following tweet:

+ + + +

Let's first clear up that Swift doesn't need to have class prefixes, because it has built-in support for namespacing through modules. From what I read, earlier Xcode 6/Swift betas did not have module support, but at least we know it's coming before 1.0 hits.

+

However, this doesn't solve any of the problems Objective-C has, and so it was surprising to see this field was removed when creating a new Objective-C project. You can still set this prefix per project after you create it, but since most Xcode templates create source files for you, those files will be created without a prefix.

+

I noticed the lack of this option, as well as the lack of a file template for Objective-C categories, back in beta 2 and immediately filed two bugs. The missing file template is as of this writing still open, but the class prefix got closed a few days later. Normally, radars aren't known for their wealth of information related to the issue the radar was filed for, but Apple Engineering made an exception this time.

+
+

After much deliberation, engineering has removed this feature.

+

This was removed intentionally. We are no longer encouraging the use of prefixes for app code, and for frameworks, you can create a prefix in the Project Inspector after creating the project.

+
+

Okay, so no more prefixes for app code. Didn't see that coming. Does Apple now encourage developers to start making extensive use of frameworks?

+
+

No. If you want to use a prefix, you can set one in the project inspector. Especially for frameworks, this is easy since the template creates no source files that should be prefixed. If you really want to prefix the classes in your main app binary, you might need to rename the few that are created initially, but the recommendation is generally that frameworks should use prefixes (in Obj-C, not in Swift) to avoid namespace issues, and apps generally don't need to.

+
+

The key part here is in the last few words: "don't need to".

+

Third-party libraries, especially frameworks that don't ship with the source code, need a prefix, because the chances of naming collisions with other code or, even worse, other libraries is significant. Users of the libraries can't (and even if they can, are likely not willing to) change the name of your classes because they collide with some other library's code.

+

"App code" on the other hand, is very unique to one particular app. It contains the UI and some of the business logic that puts your app together. Most view controllers, for example, are app code. App code is not likely to be reused, because it's very specific in what it does and is therefore unlikely to collide with other classes (assuming you name your classes well). If you think "well, this functionality may be reused in some other project", it's a great candidate for a framework!

+

I was always a big fan of the class prefixes, and while it was sometimes hard to come up with what prefix to use, I've come to actually like how they look, too. Classes that don't have a prefix feel like they're missing something. However, the guideline of "prefix frameworks, not app code" does make sense, so I will start using that from now on as well.

+
+
+ + + \ No newline at end of file diff --git a/2014/08/10/xcode-objective-c-oddities/index.html b/2014/08/10/xcode-objective-c-oddities/index.html new file mode 100644 index 0000000..b8dbe8a --- /dev/null +++ b/2014/08/10/xcode-objective-c-oddities/index.html @@ -0,0 +1,146 @@ + + + + Xcode & Objective-C Oddities - Scott Berrevoets + + + + + + + + + + + + +
+
+

Xcode & Objective-C Oddities

+ + +
+
+

Any developer that has worked with Xcode to write a little more than just "Hello, World" knows that Xcode and Objective-C have their quirks. Chances are you have heard of @TextFromXcode, the Twitter handle that portrays Xcode as a high school bully in fake text conversations like this:

+

Xcode Bully

+

But it's not always Xcode that makes Apple platform developers sometimes frown and sigh. Sometimes it's the developers themselves, sometimes it's the documentation, and sometimes a hidden gem from within Apple frameworks makes an appearance. I've compiled a short list of things I've encountered. Because it's Sunday and I didn't have anything better to do.

+

UIGobblerGestureRecognizer

+

Say what? A "gobbler" recognizer? Does it recognize when the user imitates a turkey? Well, maybe, but according to BJ Homer, it is used to avoid recognition of a gesture while animations are in progress. Ah of course! Now the name "gobbler" makes perfect sense! šŸ˜¶

+

UITapAndAHalfRecognizer

+

Speaking of gesture recognizers, there's another remarkable one that lives in the private section of UIKit: UITapAndAHalfRecognizer. This is a private subclass of UIGestureRecognizer that records "a tap and a half".

+

So what does that mean? Does it detect whether the user goes in for a second tap, but never actually touches the screen? Or does the user touch the screen ever so slightly that the second tap can hardly be considered a complete tap? Nope! This recognizer fires when a second tap stays on the screen. Not sure for what functionality Apple needs this, but it's been around since at least iOS 4, so it probably has a purpose.

+

Naming is hard

+
+

There are only two hard things in Computer Science: cache invalidation and naming things.
+- Phil Karlton

+
+

Naming is hard, but Objective-C developers always say that verbosity is one of Objective-C's strengths because its naming conventions are self-documenting. Normally I'd agree, but in this case, I think Apple may want to consider a different name.

+

HMCharacteristicValueLockMechanismLastKnownActionUnsecuredUsingPhysicalMovementExterior is a brand new constant in iOS 8 and has got to be one of the longest constants that's available in iOS. Try saying it without taking a breath in the middle.

+

I found it in the iOS 8 API diffs, but it did make me wonder. What is the longest Objective-C method available in Cocoa Touch? Well, a quick Google search led to a blog post by Stuart Sharpe, who used the Objective-C runtime to generate a list of methods in the iOS 7 SDK.

+

Turns out, the longest method in the iOS 7 SDK is

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+[CUIShapeEffectStack shapeEffectSingleBlurFrom:
+                               withInteriorFill:
+                                         offset:
+                                       blurSize:
+                                   innerGlowRed:
+                                 innerGlowGreen:
+                                  innerGlowBlue:
+                               innerGlowOpacity:
+                                 innerShadowRed:
+                               innerShadowGreen:
+                                innerShadowBlue:
+                             innerShadowOpacity:
+                                   outerGlowRed:
+                                 outerGlowGreen:
+                                  outerGlowBlue:
+                               outerGlowOpacity:
+                                 outerShadowRed:
+                               outerShadowGreen:
+                                outerShadowBlue:
+                             outerShadowOpacity:
+                            hasInsideShadowBlur:
+                           hasOutsideShadowBlur:]
+
+ +

Unless iOS 8 introduces a longer method name, this one takes the cake with 22 arguments and 352 characters. If you have Xcode configured to line-break at 80 characters (yes, some people still do that), this method signature alone, without any arguments, takes up 5 lines.

+

Another beauty of a method lives in Core Animation and its method signature is

+
1
- [CAMediaTimingFunction functionWithControlPoints::::]
+
+ +

::::? Is that even valid syntax? Yes, and you call the method like this:

+
1
[CAMediaTimingFunction functionWithControlPoints:0.25 :.50 :0 :1.0];
+
+ +

Was an array really too much to ask...?

+

[NSDate nilDate]

+

This one has made the rounds throughout the developer community before, but I'm adding it here in case you haven't seen it yet. Try running the following code:

+
1
+2
NSCalendar *calendar = [NSCalendar currentCalendar];
+[calendar components:NSCalendarUnitYear fromDate:nil toDate:[NSDate date] options:0];
+
+ +

In the console log, you'll find the following output:

+
+

*** -[__NSCFCalendar components:fromDate:toDate:options:]: fromDate cannot be nil
+I mean really, what do you think that operation is supposed to mean with a nil fromDate?
+An exception has been avoided for now.
+A few of these errors are going to be reported with this complaint, then further violations will simply silently do whatever random thing results from the nil.

+
+

Either a grumpy Apple developer woke up at the wrong side of the bed or has a great sense of humor. In any case, I wouldn't mind if more sarcastic warnings started popping up in Xcode's console.

+

Defying the Uncertainty Principle in iOS

+
+

[I]n 1927, Werner Heisenberg stated that the more precisely the position of some particle is determined, the less precisely its momentum can be known, and vice versa.
+Wikipedia

+
+

If that's true, and it's fairly safe to assume that it is, I'd avoid using -[CLLocation speed] for apps like radar detectors. I wonder if we're ever going to get a -[CLSpeed location] method, which has a more accurate speed and a less accurate location.

+

Xcode at it again

+

Lastly, the following is a screenshot I personally took at work that completely baffled me.

+

+

This would not go away, even after cleaning the build folder and rebuilding. In the end I resolved the problem by running

+
1
+2
$ git stash
+$ git reset --hard HEAD
+
+ +

Then, after a clean build and git stash pop the errors finally went away...

+

That's just a few things I've found while working with Xcode and Objective-C over the years. If you have more, please share!

+
+
+ + + \ No newline at end of file diff --git a/2014/09/15/uimenucontroller-and-the-responder-chain/index.html b/2014/09/15/uimenucontroller-and-the-responder-chain/index.html new file mode 100644 index 0000000..fe776a3 --- /dev/null +++ b/2014/09/15/uimenucontroller-and-the-responder-chain/index.html @@ -0,0 +1,63 @@ + + + + UIMenuController and the responder chain - Scott Berrevoets + + + + + + + + + + + + +
+
+

UIMenuController and the responder chain

+ + +
+
+

The responder chain is a very important paradigm in the world of iOS development, and not terribly hard to understand. Dozens of articles have been written about it, and with some examples, the concept of finding a responder to a certain event by traversing a chain of potential responders is fairly straight-forward.

+

But today it completely threw me off. The goal was very simple and has been done many times before: showing a label whose text can be copied to the clipboard. Having never worked with UIMenuController, I fired up Google and of course found an excellent NSHipster article near the top.

+

I did as the article said. I created my UILabel subclass (only accepting copy: as an action it can perform), added my gesture recognizer, and configured and showed the UIMenuController. It worked, but instead of only showing the Copy menu item, it also showed the Select All item.

+

Well, that's weird. I implemented canPerformAction:withSender: like this:

+
1
+2
+3
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+    return action == @selector(copy:);
+}
+
+ +

I made sure that this method really only returned YES for the Copy item, proceeded to explicitly return NO for selectAll: and, when that failed as well, even changed the implementation of this method to just return NO for any item. But Select All doesn't take NO for an answer, and it stubbornly showed up anyway! I was baffled and could not get rid of this Select All item.

+

I did some research, and learned that UIMenuController uses the responder chain to figure out what items to show. Furthermore, the documentation for canPerformAction:withSender: read:

+
+

This default implementation of this method returns YES if the responder class implements the requested action and calls the next responder if it does not.

+
+

That would explain the problem, if it weren't for the fact that Select All was the only other item that showed. Cut, Paste, Select, etc. did not. What was so special about Select All that no matter what I did, it always decided to make an unwanted appearance?

+

Finally I saw it. One of the last methods of the view controller that created this label had a method named, you guessed it, selectAll:. I didn't immediately connect the dots, but at least renaming that method to selectAllContacts: (which was a better name for that method anyway) got rid of Select All.

+

But how? I returned NO from my label's canPerformAction:withSender: for all actions except copy:. This means that for all other actions (see the informal UIResponderStandardEditActions), the next responder in the chain would be called. And (if necessary) the next, and the next.

+

At some point, my view controller, which participates in the responder chain, became the first responder and returned YES for the selectAll: action, simply because the method was implemented and part of UIResponderStandardEditActions.

+

So if implementing a method in that protocol is all that's needed to show the corresponding menu item, that would mean that implementing canPerformAction:withSender: in my UILabel subclass was unnecessary. I removed the method altogether and sure enough, the Copy menu item was still there. Problem solved.

+
+
+ + + \ No newline at end of file diff --git a/2014/10/20/dev-only-iphones/index.html b/2014/10/20/dev-only-iphones/index.html new file mode 100644 index 0000000..292b5ad --- /dev/null +++ b/2014/10/20/dev-only-iphones/index.html @@ -0,0 +1,49 @@ + + + + Dev-only iPhones - Scott Berrevoets + + + + + + + + + + + + +
+
+

Dev-only iPhones

+ + +
+
+

With today's release of iOS 8.1, most app developers are looking at 4 significantly different iOS versions they will be supporting: iOS 7.0., iOS 7.1, iOS 8.0, and iOS 8.1. Factoring in the number of devices (iPhone 4S, iPhone 5, iPhone 5s, iPhone 6, and iPhone 6 Plus) that run these OS versions, there is a grand total of 16 different configurations. That's without even counting the iPhone 5c, which is internally pretty much the same as the 5, or the rumored iOS 8.2 and iOS 8.3 updates.

+

Assuming an average off-contract price of $400 for these models, that means you're looking at $6,400 for 1 set of iPhones used for development. Creating a universal app? Your total will now exceed $10,000.

+

So now you have these 16 phones that will in most cases be used for testing maybe a handful of apps, that have chips and software functionality in them that will be used very little (if at all), and will most of the time just sit in some desk drawer with a dead battery. I could think of better ways to spend money.

+

This problem could have a very simple solution: dev-only iPhones. Remove or disable the cellular chip (you can simulate cellular networks using the Network Link Conditioner), give it an 8GB storage chip, remove apps such as Weather, Stocks, Calculator, Clock, etc., disable the App Store for all but the developer's own apps, allow any supported iOS version to be installed using Xcode, and sell at-cost, but only to developers in the iOS Developer Program.

+

By disabling cellular and dumbing the OS down to only include parts that developers can interact with, it becomes pretty useless for people who are just trying to look for a sweet deal on an iPhone/iPod touch. The win for developers is that they only need 1 of each device, but they're also cheaper.

+

You could even take it a step further and set memory, CPU and screen size limits (meaning only use 4" of the available 5.5"), allowing you to simulate an iPhone 5s on an iPhone 6 Plus. Now developers only need to buy the iPhone 6 Plus to be mostly covered, though I'd still recommend also getting an iPhone 5 or iPhone 5c for 32-bit devices.

+

I may be way off course here due to technical implications or limitations that I'm not aware of. There are also still cases in which you'd need an actual device on the actual software as consumers use it. Developers don't always need to test against every iOS version on every supported iOS device. But for developers who do and don't want to spend the money on all these different devices, a dev-only iPhone may be exactly what they need.

+
+
+ + + \ No newline at end of file diff --git a/2014/12/15/objc-creates-a-wrong-class-name-in-objective-c/index.html b/2014/12/15/objc-creates-a-wrong-class-name-in-objective-c/index.html new file mode 100644 index 0000000..5bb3b21 --- /dev/null +++ b/2014/12/15/objc-creates-a-wrong-class-name-in-objective-c/index.html @@ -0,0 +1,59 @@ + + + + @objc creates a wrong class name in Objective-C - Scott Berrevoets + + + + + + + + + + + + +
+
+

@objc creates a wrong class name in Objective-C

+ + +
+
+

A few months ago, I decided I'd get started porting SDCAlertView to Swift, but I was only a few minutes in until I ran into a problem I had no idea how to solve: I couldn't get my class names right in both Swift and Objective-C. Even though there are copious amounts of documentation covering the interoperability of Swift and Objective-C, somehow I couldn't get it to work and I brushed it off as me being stupid.

+

Tonight I tried again, and after some more research it turned out that the behavior I saw was actually a bug in the compiler! I'm surprised that the latest Xcode version is 6.1.1 and that apparently not enough people have run into this problem for Apple to make it a priority.

+

The bug can easily be reproduced by creating an Objective-C project in Xcode, and then adding the following Swift class:

+
1
+2
+3
+4
+5
import UIKit
+
+@objc(SDCMyLabel)
+class MyLabel: UILabel {
+}
+
+ +

You would expect that, once you import MyProject-Swift.h in your Objective-C class, you could instantiate a class named SDCMyLabel. However, instead, you can only instantiate a class named MyLabel.

+

I found this very recent Stack Overflow question which pretty much asks the same thing. "milos" gives a great answer by explaining the observed behavior in Xcode 6.0.1, and the expected behavior according to the WWDC session video.

+

After reading the answer and watching the relevant parts of the vide, I slightly changed my previously-drawn conclusion from "I'm stupid" to "Xcode's stupid" and filed rdar://19261044. If you're running into the same problem, please duplicate and cross your fingers the engineers at the fruit company will fix it soon.

+
+
+ + + \ No newline at end of file diff --git a/2015/06/13/the-power-of-uistackview/index.html b/2015/06/13/the-power-of-uistackview/index.html new file mode 100644 index 0000000..639e93a --- /dev/null +++ b/2015/06/13/the-power-of-uistackview/index.html @@ -0,0 +1,61 @@ + + + + The power of UIStackView - Scott Berrevoets + + + + + + + + + + + + +
+
+

The power of UIStackView

+ + +
+
+

I was in the audience for "Implementing UI Designs in Interface Builder" at WWDC, where Apple touted UIStackView as a new, better way to lay out your views that in a lot of cases didn't require Auto Layout.

+

While the presentation looked good, I wasn't quite sure this solved a real problem, as the required constraints weren't too complicated to build in the first pace. However, after I found some time to play around with UIStackView, I'm convinced. This view is very, very powerful and can make building common UIs a lot simpler. Take, for example, a number grid:

+
1
+2
+3
+4
[1] [2] [3]
+[4] [5] [6]
+[7] [8] [9]
+    [0]
+
+ +

It would take a while to build this kind of UI in Auto Layout. You would have to create constraints between individual labels, make the labels the same size everywhere, make the spacing between all the labels equal, and avoid hardcoding any numbers to keep the whole thing adaptive. This can be a tedious process in Interface Builder, but imagine you're building this in code. It would take forever, especially after debugging your UI.

+

With UIStackView, doing all of this takes about 30 seconds. After dragging out all the labels, you just need to embed every row in its own stack view. Set the distribution (in code that's UIStackViewDistribution) for these stack views to "fill equally" (.FillEqually), then embed all four stack views in their own stack view.

+

UIStackView example

+

The top-level, vertical stack view should get top/leading/trailing/bottom space to its superview (or the layout guides), the 4 horizontal stack views an "equal width" constraint to their superview. Finally, center the text of the individual labels (you can select all labels at once and do this with 1 action). Done - that's it.

+

But it gets better. Say you want to conditionally show or hide the 0 label at the bottom. I don't know why you would want that, but just go with it šŸ˜›

+

To do this, all you have to is toggle the hidden property of that label. The containing stack view will automatically reposition its subviews. If you put that line in an animation block, it will reposition its subviews in an animated fashion.

+

That last part is a very welcome feature for developers that work with a lot of dynamic views. Instead of creating redundant constraints with lower priorities, removing views from their view hierarchies, and re-adding the view and all its constraints if the new views needs to be re-shown, you can simply toggle hidden instead.

+

As more people get their hands on UIStackView I'm sure it will show off more of its powers, but needless to say I'm sold. Too bad I can't use it for a while...

+
+
+ + + \ No newline at end of file diff --git a/2015/07/23/objc-class-prefixes-fixed-in-xcode-7-beta-4/index.html b/2015/07/23/objc-class-prefixes-fixed-in-xcode-7-beta-4/index.html new file mode 100644 index 0000000..a9b294c --- /dev/null +++ b/2015/07/23/objc-class-prefixes-fixed-in-xcode-7-beta-4/index.html @@ -0,0 +1,65 @@ + + + + @objc class prefixes fixed in Xcode 7 beta 4 - Scott Berrevoets + + + + + + + + + + + + +
+
+

@objc class prefixes fixed in Xcode 7 beta 4

+ + +
+
+

Back in December I wrote about what I thought was a bug in the Swift compiler that would expose the wrong class name for a Swift class in Objective-C. I then later found out everything worked as intended and I had just misunderstood what @objc() exactly did. Apparently it was never supposed to modify the class name at compile time, but only at runtime.

+

I'm sure changing the class name just at runtime has its uses, but in my opinion, this would be most helpful if it also affected the compile time name of Objective-C classes. It allows you to namespace your classes in Objective-C using your three-letter prefix, without needing that prefix in Swift because you could namespace by way of modules.

+

And fortunately, in Xcode 7 beta 4, they have actually modified the @objc() notation so that it does do this. For example, if I have a Swift module that I want others to be able to use in their Objective-C codebase, I could write a class like this:

+
1
+2
+3
+4
@objc(SDCMyClass)
+class MyClass: NSObject {
+    // ...
+}
+
+ +

And in MyProject-Swift.h the Objective-C class is defined as:

+
1
+2
+3
+4
SWIFT_CLASS_NAMED("MyClass")
+@interface SDCMyClass : NSObject
+- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
+@end
+
+ +

In Swift I can simply use the class as MyClass, but in Objective-C its name is SDCMyClass, which ensures it doesn't collide with other classes named MyClass. Needless to say, I'm very happy they changed this behavior, it makes much more sense now.

+
+
+ + + \ No newline at end of file diff --git a/2016/03/21/outlets-strong-or-weak/index.html b/2016/03/21/outlets-strong-or-weak/index.html new file mode 100644 index 0000000..13156ee --- /dev/null +++ b/2016/03/21/outlets-strong-or-weak/index.html @@ -0,0 +1,67 @@ + + + + Outlets: strong! or weak? - Scott Berrevoets + + + + + + + + + + + +
+
+

Outlets: strong! or weak?

+ + +
+
+

There are a lot of styles out there when it comes to using Interface Builder outlets in Swift. Even Apple's documentation and sample code isn't always consistent. The most common one, the one Apple uses in its sample code, follows this pattern:

+

@IBOutlet private weak var someLabel: UILabel!

+

Let's break this down by keyword:

+ +

While at first this seems like a solid approach, at Lyft we quickly realized we weren't fans of this one-size-fits-all way of defining outlets. Instead, the behavior and consequences of the different elements should define the outlet's exact syntax, just like any other variable.

+

For example, if there is a code path that removes an outlet from its superview, or the outlet is (intentionally) not hooked up in the storyboard, it needs to be an optional because the outlet is not guaranteed to be there when it's accessed.

+

@IBOutlet private var someLabel: UILabel?

+

If there is no code path that re-adds the outlet to the view hierarchy, it would also be good to make it weak to not hold on to it unnecessarily when it gets removed:

+

@IBOutlet private weak var someLabel: UILabel?

+

This ensures that if the label is removed from the superview, it's not being kept in memory by the strong reference in the view controller. In the most common case, where there is an outlet that will always be there, a strong, implicitly unwrapped optional is appropriate:

+

@IBOutlet private var someLabel: UILabel!

+

The outlet isn't weak in case the code ever changes so that there is a code path that removes the view from the view hierarchy but you forget to update the optionality of the property. The object will stay in memory and using it won't crash your app.

+

These examples all follow 3 simple rules:

+
    +
  1. ! needs a guarantee that the view exists, so always use strong to provide that guarantee
  2. +
  3. If it's possible the view isn't part of the view hierarchy, use ? and appropriate optional-handling (optional binding/chaining) for safety
  4. +
  5. If you don't need a view anymore after removing it from the view hierarchy, use weak so it gets removed from memory.
  6. +
+

Applying these three rules means you properly use the optional semantics. After all, using ! for a view that may not exist is no different than defining any other property as an implicitly unwrapped optional that may not exist.

+
+
+ + + \ No newline at end of file diff --git a/2016/08/01/silencing-nslog/index.html b/2016/08/01/silencing-nslog/index.html new file mode 100644 index 0000000..f33aba9 --- /dev/null +++ b/2016/08/01/silencing-nslog/index.html @@ -0,0 +1,174 @@ + + + + Silencing NSLog - Scott Berrevoets + + + + + + + + + + + +
+
+

Silencing NSLog

+ + +
+
+

When your app has a lot of third-party dependencies, what often happens is that +those libraries log a bunch of things to the Xcode console to help their own +debugging. Unfortunately, a lot of these logs are useful only to the developers +of the library, but not the developers of apps that integrate the library. For +example, they log things like <SomeLibrary> (version 1.2.3) initialized, or +<SomeLibrary> started <primary functionality>, sometimes with a long list of +parameters or input sources that are irrelevant to you.

+

Finding your own log statements in a jungle of other logs can then be +very difficult and adds to the frustration of not being able to work the debugger +as you would like to.

+

If a library is open source you can suggest a change by removing the log or +otherwise make it less obtrusive. However, if your change gets accepted at all, +that doesn't solve the immediate problem of being able to debug your own code +using the console.

+

Meet _NSSetLogCStringFunction(). This C function has been around in Foundation +for a long time, and while there is some +documentation on it, it's +still a private method. However, that doesn't mean you can't use it in debug +mode when your logs are the most valuable!

+

In short, this function lets you set a function pointer that can log C strings, +which NSLog then uses instead of the normal implementation. You can do this +in two ways.

+

The first one is by adding this to your Objective-C bridging header:

+
1
+2
+3
#if DEBUG
+extern void _NSSetLogCStringFunction(void(*)(const char*, unsigned, BOOL));
+#endif
+
+ +

and then use it like this:

+
1
+2
+3
+4
+5
+6
func disableNSLog() {
+#if DEBUG
+    _NSSetLogCStringFunction { message, _, _ in
+        // no actual logging, message just gets lost
+    }
+#endif
+
+ +

If you want to stick to pure Swift, you can do so by adding this to your code +somewhere:

+
1
+2
@_silgen_name("_NSSetLogCStringFunction")
+func setNSLogFunction(_: @convention(c) (UnsafePointer<Int8>, UInt32, ObjCBool) -> Void)
+
+ +

and then use it like this:

+
1
+2
+3
+4
+5
+6
+7
func disableNSLog() {
+#if DEBUG
+    setNSLogFunction { message, _, _ in
+        // no actual logging, message just gets lost
+    }
+#endif
+}
+
+ +

Obviously, you can do anything you want inside the closure, including writing to +a file, annotating the message with a date/time, passing it to your custom +logging library, etc.

+

One downside of this is that Apple's frameworks use NSLog extensively as well, +so in the above case of completely disabling logging, helpful messages get lost +as well. You won't be able to use NSLog yourself either anymore, so I suggest +you use print() or a custom logging framework that's not NSLog based.

+

If you're not afraid of doing (more) horrible things in your codebase, you can +avoid losing Apple frameworks' messages by parsing the stack trace and looking +at the framework that called this function and see if it's something you want to +let through:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
func disableNSLog() {
+#if DEBUG
+    _NSSetLogCStringFunction { message, _, _ in
+        // message is of type UnsafePointer<Int8> so first see if we can get a
+        // normal String from that. Safety first!
+
+        guard let message = String.fromCString(message) else {
+            return
+        }
+
+        let callStack = NSThread.callStackSymbols()
+        let sourceString = callStack[6]
+        let separatorSet = NSCharacterSet(charactersInString: " -[]+?.,")
+        let stackFrame = sourceString.componentsSeparatedByCharactersInSet(separatorSet)
+        let frameworkName = stackFrame[3]
+
+        if frameworkName == "UIKit" || frameworkName == "Foundation" {
+            MyCustomLogger.log(message)
+        }
+    }
+#endif
+}
+
+ +

This discards all logs, except if they're coming from UIKit or Foundation. +The stack trace parsing is by no means safe (its format could change, for +example), but since it's wrapped in #if DEBUG directives it won't mess with +anything in the App Store build.

+

Note that static libraries are part of your main app's target, which means you +have to filter out logs from your own target to hide those.

+

You could even go a bit farther and check the message for keywords you like or +don't like and make a decision on whether you want to log or not. Keep in mind, +though, that any work you do here needs to be fast as you don't always know just +how much is being logged.

+
+
+ + + \ No newline at end of file diff --git a/2017/03/06/using-interface-builder-at-lyft/index.html b/2017/03/06/using-interface-builder-at-lyft/index.html new file mode 100644 index 0000000..0564e44 --- /dev/null +++ b/2017/03/06/using-interface-builder-at-lyft/index.html @@ -0,0 +1,222 @@ + + + + Using Interface Builder at Lyft - Scott Berrevoets + + + + + + + + + + + +
+
+

Using Interface Builder at Lyft

+ + +
+
+

Last week people realized that Xcode 8.3 by default uses storyboards in new +projects without a checkbox to turn this off. This of course sparked the +Interface Builder vs. programmatic UI discussion again, so I wanted to give some +insight in our experience using Interface Builder in building the Lyft app. This +is not intended as hard "you should also use Interface Builder" advice, but +rather to show that IB can work at a larger scale.

+

First, some stats about the Lyft app:

+ +

With the +rewrite of +our app we moved to using IB for about 95% of our UI.

+

The #1 complaint about using Interface Builder for a project with more than 1 +developer is that it's impossible to resolve merge conflicts. We never have +this problem. Everybody on the team can attest that they have never run into +major conflicts they couldn't reasonably resolve.

+

With that concern out of the way, what about some of the other common criticisms +Interface Builder regularly gets?

+

Improving the workflow

+

Out of the box, IB has a number of shortcomings that could make working with it +more painful than it needs to be. For example, referencing IB objects from code +still can only be done with string identifiers. There is also no easy way to +embed custom views (designed in IB) in other custom views.

+

Over time we have improved the workflow for our developers to mitigate some of +these shortcomings, either by writing some tools or by writing a little bit of +code that can be used project-wide.

+

storyboarder script

+

To solve the issue of stringly-typed view controller identifiers, we wrote a +script that, just before compiling the app, generates a struct with static +properties that exposes all view controllers from the app in a strongly-typed +manner. This means that now we can instantiate a view controller in code like +this:

+
1
let viewController = Onboarding.SignUp.instantiate()
+
+ +

Not only is viewController now guaranteed to be there at runtime (if something +is wrong in the setup of IB the code won't even compile), but it's also +recognized as a SignUpViewController and not a generic UIViewController.

+

Strongly-typed segues

+

All our view controllers have a base view controller named ViewController. +This base controller implements prepare(for:sender:) like this:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
open override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
+    guard let identifier = segue.identifier else {
+        return
+    }
+
+    let segueName = identifier.firstLetterUpperString()
+    let selector = Selector("prepareFor\(segueName):sender:")
+    if self.responds(to: selector) {
+        self.perform(selector, with: segue.destination, with: sender)
+    }
+}
+
+ +

This means that a view controller that has a segue to +TermsOfServiceViewController can now do this:

+
1
+2
+3
+4
@objc
+private func prepareForTermsOfService(_ viewController: TermsOfServiceViewController, sender: Any?) {
+    viewController.onTermsAccepted = { [weak self] self?.proceed() }
+}
+
+ +

We no longer have to implement prepareForSegue and then switch on the +segue's identifier or destination controller, but we can implement a separate +method for every segue from this view controller instead which makes the code +much more readable.

+

NibView

+

We wrote a NibView class to make it more convenient to embed custom views in +other views from IB. We marked this class with @IBDesignable so that it knows +to render itself in IB. All we have to do is drag out a regular UIView from +the object library and change its class. If there is a XIB with the same name as +the class, NibView will automatically instantiate it and render it in the +canvas at design time and on screen at runtime.

+

Every standalone view we design in IB (which effectively means every view in our +app) inherits from NibView so we can have an "unlimited" number of nested +views show up and see the final result.

+

Basic @IBDesignables

+

Since a lot of our views have corner radii and borders, we have created this +UIView extension:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
public extension UIView {
+    @IBInspectable public var cornerRadius: CGFloat {
+        get { return self.layer.cornerRadius }
+        set { self.layer.cornerRadius = newValue }
+    }
+
+    @IBInspectable public var borderWidth: CGFloat {
+        get { return self.layer.borderWidth }
+        set { self.layer.borderWidth = newValue }
+    }
+
+    @IBInspectable public var borderColor: UIColor {
+        get { return UIColor(cgColor: self.layer.borderColor!) }
+        set { self.layer.borderColor = newValue.cgColor }
+    }
+}
+
+ +

This lets us easily set these properties on any view (including the ones from +UIKit) from Interface Builder.

+

Linter

+

We wrote a linter to make sure views are not misplaced, have +accessibility labels, trait variations are disabled (since we only officially +support portrait mode on iPhone), etc.

+

ibunfuck

+

A bug impacting developers +that use Interface Builder on both Retina and non-Retina screens (which at Lyft +is every developer) has caused us enough grief to write +ibunfuck - a tool to remove +unwanted changes from IB files.

+

Color palette

+

We created a custom color palette with the commonly used colors in our app so +it's easy to select these colors when building a new UI. The color names in the +palette follow the same names designers use when they give us new designs, so +it's easy to refer to and use without having to copy RGB or hex values.

+

Our approach

+

In addition to these tools and project-level improvements, we have a number of +"rules" around our use of IB to keep things sane:

+ +

Of course, even with these improvements everything is not peaches and cream. +There are definitely still problems. New versions of Xcode often change the XML +representation which leads to a noisy diff. Some properties can simply not be +set in IB meaning we're forced to break our "do everything in IB" rule. +Interface Builder has bugs we can't always work around.

+

However, with our improved infrastructure and the points from above, we are +happy with how IB works for us. We don't have to write tons of Auto Layout code +(which would be incredibly painful due to the nature of our UIs), get a visual +representation of how a view looks without having to run the app after every +minor change, and maybe one day we can get our designers make changes to our UI +without developers' help.

+
+
+ + + \ No newline at end of file diff --git a/2018/08/08/re-binding-self-the-debuggers-breaking-point/index.html b/2018/08/08/re-binding-self-the-debuggers-breaking-point/index.html new file mode 100644 index 0000000..6252742 --- /dev/null +++ b/2018/08/08/re-binding-self-the-debuggers-breaking-point/index.html @@ -0,0 +1,143 @@ + + + + Re-binding self: the debugger's break(ing) point - Scott Berrevoets + + + + + + + + + + + +
+
+

Re-binding self: the debugger's break(ing) point

+ + +
+
+

Update 07-29-2019: The bug described below is fixed in Xcode 11 so this blog +post has become irrelevant. I'm leaving it up for historical purposes.

+

For the Objective-C veterans in the audience, the strong-self-weak-self dance is +a practice mastered early on and one that is used very frequently. There are a +lot of different incantations, but the most basic one goes something like this:

+
1
+2
+3
+4
__weak typeof(self) weakSelf = self;
+dispatch_group_async(dispatch_get_main_queue(), ^{
+    [weakSelf doSomething];
+});
+
+ +

Then, if you needed a strong reference to self again inside the block, you'd +change it to this:

+
1
+2
+3
+4
+5
__weak typeof(self) weakSelf = self;
+dispatch_group_async(dispatch_get_main_queue(), ^{
+    typeof(weakSelf) strongSelf = weakSelf;
+    [strongSelf.someOtherObject doSomethingWith:strongSelf];
+});
+
+ +

Fortunately, this was much easier on day 1 of Swift when using the [weak self] +directive:

+
1
+2
+3
+4
+5
DispatchQueue.main.async { [weak self] in
+    if let strongSelf = self {
+        strongSelf.someOtherObject.doSomething(with: strongSelf)
+    }
+}
+
+ +

self is now weak inside the closure, making it an optional. Unwrapping it +into strongSelf makes it a non-optional while still avoiding a retain cycle. +It doesn't feel very Swifty, but it's not terrible.

+

More recently, it's become known that Swift supports re-binding self if you +wrap it in backticks. That makes for an arguably much nicer syntax:

+
1
+2
+3
+4
DispatchQueue.main.async { [weak self] in
+    guard let `self` = self else { return }
+    self.someOtherObject.doSomething(with: self)
+}
+
+ +

This was long considered, and confirmed to be, a hack that worked due to a bug in the compiler, but +since it worked and there weren't plans to remove it, people (including us at +Lyft) started treating it as a feature.

+

However, there is one big caveat: the debugger is entirely hosed for +anything you do in that closure. Ever seen an error like this in your Xcode +console?

+
1
+2
+3
error: warning: <EXPR>:12:9: warning: initialization of variable '$__lldb_error_result' was never used; consider replacing with assignment to '_' or removing it
+    var $__lldb_error_result = __lldb_tmp_error
+        ~~~~^~~~~~~~~~~~~~~~~~~~
+
+ +

That's because self was re-bound. This is easy to reproduce: create a new +Xcode project and add the following snippet to viewDidLoad():

+
1
+2
+3
+4
+5
+6
DispatchQueue.main.async { [weak self] in
+    guard let `self` = self else { return }
+
+    let description = self.description
+    print(description) // set a breakpoint here
+}
+
+ +

When the breakpoint hits, execute (lldb) po description and you'll see the +error from above. Note that you're not even using self - merely re-binding +it makes the debugger entirely useless inside that scope.

+

People with way more knowledge of LLDB than I do can explain this in more detail +(and have), but the gist is that the +debugger doesn't like self's type changing. At the beginning of the closure +scope, the debugging context assumes that self's type is Optional, but it is +then re-bound to a non-optional, which the debugger doesn't know how to handle. +It's actually pretty surprising the compiler supports changing a variable's type +at all.

+

Because of this problem, at Lyft we have decided to eliminate this pattern +entirely in our codebases, and instead re-bind self to a variable named +this.

+

If you do continue to use this pattern, note that in a +discussion +on the Swift forums many people agreed that re-binding self should be +supported by the language without the need for backticks. The pull +request was merged shortly after and +with the release of Swift 4.2 in the fall, you'll be able to use guard let self += self else { return } (at the cost of losing debugging capabilities!)

+
+
+ + + \ No newline at end of file diff --git a/2021/10/14/ios-architecture-at-lyft/index.html b/2021/10/14/ios-architecture-at-lyft/index.html new file mode 100644 index 0000000..52547a5 --- /dev/null +++ b/2021/10/14/ios-architecture-at-lyft/index.html @@ -0,0 +1,428 @@ + + + + iOS Architecture at Lyft - Scott Berrevoets + + + + + + + + + + + +
+
+

iOS Architecture at Lyft

+ + +
+
+

June 30, 2014 was my first day at Lyft as the first iOS hire on the ~3 person +team. The app was written in Objective-C, and the architecture was a 5000-line +nested switch statement.

+

Since then, the team has grown to about 70 people and the codebase to 1.5M lines +of code. This required some major changes to how we architect our code, and +since it had been a while since we've given an update like this, now seems as +good a time as any.

+

Requirements

+

The effort to overhaul and modernize the architecture began around mid-2017. +We started to reach the limits of the patterns we established in the 2015 +rewrite of the app, and it was clear the codebase and the team would continue to +grow and probably more rapidly than it had in the past.

+

The primary problems that the lack of a more mature architecture presented and +that we wanted to solve were:

+ +

There was not going to be one solution that would solve all of this inherently, +but over the course of a few years we developed a number of processes and +technical solutions to reduce these problems.

+

Modules

+

First, to provide better feature separation, we introduced modules. Every +feature had its own module, with its own test suite, that could be developed in +isolation from other modules. This forced us to think more about public APIs and +hiding implementation details behind them. Compile times improved, and it +required much less collaboration with other teams to make changes.

+

We also introduced an ownership model that ensured each module has at least one +team that's responsible for that module's tech debt, documentation, etc.

+

Module types

+

After fully modularizing the app and having 700 modules worth of code, we took +this a step further and introduced a number of module types that each module +would follow.

+ +

Breaking modules down this way enabled us to implement dependency validators: we +can validate that certain modules can't depend on others. For example, a logic +module can't depend on a UI module, and a Service module can't import UIKit.

+

This module structure also prevents complicated circular dependencies, +e.g. a Coupons module depending on Payments and vice versa. Instead, the +Payments module can now import CouponsUI without needing to import the full +Coupons feature. It's led to micromodules in some areas, but we've generally +been able to provide good tooling to make this easier to deal with.

+

All in all we now have almost 2000 modules total for all Lyft apps.

+

Dependency Injection

+

Module types solved many of our dependency tree problems at the module level, +but we also needed something more scalable than singletons at the code level.

+

For that we've built a lightweight dependency injection framework which we +detailed in a SLUG talk. It resembles a service locator pattern, with a +basic dictionary mapping protocols to instantiations:

+
1
+2
let getNetworkCommunicator: NetworkCommunicating =
+    bind(NetworkCommunicating.self, to: { NetworkCommunicator() })
+
+ +

The implementation of bind() doesn't immediately return NetworkCommunicator, +but requires the object be mocked if we're in a testing environment:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
let productionInstantiators: [ObjectIdentifier: () -> Any] = [:]
+let mockedInstantiators: [ObjectIdentifier: () -> Any] = [:]
+
+func bind<T>(protocol: T.Type, instantiator: () -> T) -> T {
+    let identifier = ObjectIdentifier(T.self)
+
+    if NSClassFromString("XCTestCase") == nil {
+        return productionInstantiators[identifier] ?? instantiator()
+    } else {
+        return mockedInstantiators[identifier]!
+    }
+}
+
+ +

In tests, the mock is required or the test will crash:

+
1
+2
+3
+4
+5
+6
+7
+8
+9
final class NetworkingTests: XCTestCase {
+    private var communicator = MockNetworkCommunicator()
+
+    func testNetworkCommunications() {
+        mock(NetworkCommunicating.self) { self.communicator }
+
+        // ...
+    }
+}
+
+ +

This brings two benefits:

+
    +
  1. It forces developers to mock objects in tests, avoiding production side + effects like making network requests
  2. +
  3. It provided a gradual adoption path rather than updating the entire app at + once through some more advanced system
  4. +
+

Although this framework has some of the same problems as other Service +Locator implementations, it works well enough for us and the limitations are +generally acceptable.

+

Flows

+

Flows, inspired by Square's Workflow, are the backbone of all Lyft apps. +Flows define the navigation rules around a number of related screens the user +can navigate to. The term flow was already common in everyday communications +("after finishing the in-ride flow we present the user with the payments flow") +so this terminology mapped nicely to familiar terminology.

+

Flows rely on state-driven routers that can either show a screen, or route to +other routers that driven by different state. This makes them easy to compose, +which promoted the goal of feature isolation.

+

At the core of flows lies the Routable protocol:

+
1
+2
+3
protocol Routable {
+    let viewController: UIViewController
+}
+
+ +

It just has to be able to produce a view controller. The (simplified) router +part of a flow is implemented like this:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
final class Router<State> {
+    private let routes: [(condition: (State) -> Bool, routable: Routable?)]
+
+    func addRoute(routable: Routable?, _ condition: @escaping (State) -> Bool) {
+        self.routes.append((condition, routable))
+    }
+
+    func route(for state: State) -> Routable? {
+        self.routes.first { $0.condition(state) }
+    }
+}
+
+ +

In other words: it takes a bunch of rules where if the condition is true +(accepting the flow's state as input) it provides a Routable. Each flow defines +its own possible routes and matches those to a Routable:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
struct OnboardingState {
+    let phoneNumber: String?
+    let verificationCode: String?
+    let email: String?
+}
+
+final class OnboardingFlow {
+    private let router = Router<OnboardingState>
+    private let state = OnboardingState()
+
+    init() {
+        self.router.addRoute({ $0.phoneNumber == nil }, EnterPhoneNumberViewController())
+        self.router.addRoute({ $0.verificationCode == nil }, VerifyPhoneViewController())
+        self.router.addRoute({ $0.email == nil }, EnterEmailViewController())
+
+        // If all login details are provided, return  `nil` to indicate this flow has
+        // no (other) Routable to provide and should be exited
+        self.router.addRoute({ _ in }, nil)
+    }
+
+    func currentRoutable() -> Routable {
+        return self.router.route(for: state)
+    }
+}
+
+ +

We're then composing flows by adding Routable conformance to each flow and +have it provide a view controller, adding its current Routables view +controller as a child:

+
1
+2
+3
+4
+5
+6
+7
extension Flow: Routable {
+    var rootViewController: UIViewController {
+        let parent = UIViewController()
+        parent.addChild(self.currentRoutable().viewController)
+        return parent
+    }
+}
+
+ +

Now a flow can also route to another flow by adding an entry to its router:

+
1
self.router.addRoute({ $0.needsOnboarding }, OnboardingFlow())
+
+ +

This pattern could let you build entire trees of flows:

+

Simplified flow diagram

+

When we first conceptualized flows we imagined having a tree of about 20 flows +total; today we have more than 80. Flows have become the "unit of development" +of our apps: developers no longer need to care about the full application or a +single module, but can build an ad-hoc app with just the flow they're working +on.

+

Plugins

+

Although flows simplify state management and navigation, the logic of the +individual screens within a flow could still be very intertwined. To mitigate +that problem, we've introduced plugins. Plugins allow for attaching +functionality to a flow without the flow even knowing that the plugin exists.

+

For example, to add more screens to the OnboardingFlow from above, we can expose +a method on it that would call into its router:

+
1
+2
+3
+4
+5
+6
+7
+8
extension OnboardingFlow {
+    public func addRoutingPlugin(
+        routable: Routable?,
+        _ condition: @escaping (OnboardingState) -> Bool)
+    {
+        self.router.addRoute((condition, routable))
+    }
+}
+
+ +

Since this method is public, any plugin that imports it can add a new screen. The +flow doesn't know anything about this plugin, so the entire dependency tree is +inverted with plugins. Instead of a flow depending on all the functionalities of +all of its plugins, it provides a simple interface that lets plugins extend this +functionality in isolation by having them depend on the flow.

+

Simplified plugin setup

+

Since all Lyft apps operate on a tree of flows, the overall dependency graph +changes from a tree shape to a "bubble" shape:

+

Bubble dependency graph

+

This setup provides feature isolation at the compiler level which makes it much +harder to accidentally intertwine features. Each plugin also has its own feature +flag, making it very easy to disable a feature if necessary.

+

In addition to routing plugins, we also provide interfaces to add additional +views to any view controller, deep link plugins to deep link to any arbitrary +part of the app, list plugins to build lists with custom content, and a few +others very unique to Lyft's use cases.

+

Unidirectional Data Flow

+

More recently we introduced a redux-like unidirectional data flow (UDF) for +screens and views within flows. Flows were optimized for state management within +a collection of screens, the UDF brings the same benefits we saw there to +individual screens.

+

A typical redux implementation has state flowing into the UI and actions that +modify state coming out of the UI. Influenced by The Composable +Architecture, our implementation of redux actions also includes executing +side effects to interact with the environment (network, disk, notifications, +etc.).

+

Declarative UI

+

In 2018, we began building out our Design System. At the time, it was a +layer on top of UIKit, often with a slightly modernized API, that would provide +UI elements with common defaults like fonts, colors, icons, dimensions, etc.

+

When Apple introduced SwiftUI in mid-2019, it required a deployment target of +iOS 13. At the time, we still supported iOS 10 and even today we still support +iOS 12 so we still can't use it.

+

However, we did write an internal library called DeclarativeUI, which provides +the same declarative APIs that SwiftUI brings but leveraging the Design System +we had already built. Even better, we've built binding conveniences into both +DeclarativeUI and our UDF Store types to make them work together seamlessly:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
import DeclarativeUI
+import Unidirectional
+
+final class QuestionView: DeclarativeUI.View {
+    private let viewStore: Store<QuestionState>
+
+    init(store: Store<QuestionState>) {
+        self.viewStore = store
+    }
+
+    var body: DeclarativeUI.View {
+        return VStackView(spacing: .three) {
+            HeaderView(store: self.store)
+            Label(text: viewStore.bind(\.header))
+                .textStyle(.titleF1)
+                .textAlignment(.center)
+                .lineLimit(nil)
+                .accessibility(postScreenChanged: viewStore.bind(\.header))
+            VStackView(viewStore.bind(\.choices), spacing: .two) { choice in
+                TwoChoiceButton(choice: choice).onEvent(
+                    .touchUpInside,
+                    action: viewStore.send(.choiseSelected(index: choice.index)))
+            }
+            .hidden(viewStore.bind(\.choices.isEmpty))
+
+            if viewStore.currentState.model.usesButtonToIncrementQuestion {
+                NextQuestionButton(store: self.store)
+                    .hidden(viewStore.bind(\.choices.isEmpty))
+            }
+        }
+    }
+}
+
+ +

Putting it all together

+

All these technologies combined make for a completely different developer +experience now than five years ago. Doing the right thing is easy, doing the +wrong thing is difficult. Features are isolated from each other, and even +feature components are separated from each other in different modules.

+

Testing was never easier: unit tests for modules with pure business logic, +snapshot tests for UI modules, and for integration tests it takes little effort +to sping up a standalone app with just the flow you're interested in.

+

State is easy to track with debug conveniences built into the architectures, +building UI is more enjoyable than it was with plain UIKit, and adding a feature +from 1 app into another is often just a matter of attaching the plugin to a +second flow without detangling it from all other features on that screen.

+

It's amazing to look back at where the codebase started some 6 years ago, and +where it is now. Who knows where it will be in another 6 years!

+

Note: If you're interested in hearing more, I also talked about many of these +technologies on the Lyft Mobile Podcast!

+
+
+ + + \ No newline at end of file diff --git a/2022/07/15/third-party-libraries-are-no-party-at-all/index.html b/2022/07/15/third-party-libraries-are-no-party-at-all/index.html new file mode 100644 index 0000000..713e972 --- /dev/null +++ b/2022/07/15/third-party-libraries-are-no-party-at-all/index.html @@ -0,0 +1,193 @@ + + + + Third-party libraries are no party at all - Scott Berrevoets + + + + + + + + + + + +
+
+

Third-party libraries are no party at all

+ + +
+
+

What better way to end the week than with a hot take?

+

In my 8 years at Lyft, product managers or engineers have often wanted to add +third-party libraries to one of our apps. Sometimes itā€™s necessary to integrate +with a specific vendor (like PayPal), sometimes itā€™s to avoid having to build +something complicated, and sometimes itā€™s simply to not reinvent the wheel.

+

While these are generally reasonable considerations, the risks and associated +costs of using a third-party library are often overlooked or misunderstood. In +some cases the risk is worth it, but to be able to determine that you first need +to be able to define that risk accurately. To make that risk assessment more +transparent and consistent, we defined a process of things we look at to +determine how much risk we're incurring by integrating it and shipping it in one +or more production apps.

+

Risks

+

Most larger organizations, including ours, have some form of code review as part +of their development practices. For those teams, adding a third-party library is +equivalent to adding a bunch of unreviewed code written by someone who doesn't +work on the team, subverting the standards upheld during code review and +shipping code of unknown quality. This introduces risk in how the app runs, +long-term development of the app, and, for larger teams, overall business risk.

+

Runtime risks

+

Library code generally has the same level of access to system resources as +general app code, but they don't necessarily apply the best practices the +team put in place for managing these resources. This means they have access +to the disk, network, memory, CPU, etc. without any restrictions or +limitations, so they can (over)write files to disk, be memory or CPU hogs +with unoptimized code, cause dead locks or main thread delays, download (and +upload!) tons of data, etc. Worse, they can cause crashes or even crash +loops. Twice.

+

Many of these situations aren't discovered until the app is already available to +customers, at which point fixing it requires creating a new build and going +through the review process which is often time intensive and costly. The risk +can be somewhat mitigated by invoking the library behind a feature flag, but +that isn't a silver bullet either (see below).

+

Development risks

+

To quote a coworker: "every line of code is a liability", and this is even more +true for code you didn't write yourself. Libraries could be slow in adopting new +technologies or APIs holding the codebase back, or too fast causing a deployment +target that's too high. When Apple and Google introduce new OS versions each +year, they often require developers update their code based on changes in their +SDKs, and library developers have to follow suit. This requires coordinated +efforts, alignment in priorities, and the ability to get the work done in a +timely manner.

+

As the mobile platforms are ever-changing this becomes a continuous, ongoing +risk, compounded by the problem that teams and organizations aren't static +either. When a library that was integrated by a team that no longer exists needs +to be updated, it takes a long time to figure out who should do so. It has +proven extremely rare and extremely difficult to remove a library once it's +there, so we treat it as a long-term maintenance cost.

+

Business risks

+

As I mentioned above, modern OSes make no distinction between app code and +library code, so in addition to system resources they also have access to user +information. As app developers we're responsible for using that information +properly, and any libraries are part of that responsibility.

+

If the user grants location access to the Lyft app, any third-party library +automatically gets access too. They could then upload that data to their own +servers, competitors' servers, or who knows where else. This is even more +problematic when a library needs a new permission we didn't already have.

+

Similarly, a system is as secure as its weakest link but if you include +unreviewed, unknown code you have no idea how secure it really is. Your +well-designed secure coding practices could all be undone by one misbehaving +library. The same goes for any policies Apple and Google put in place like "you +are not allowed to fingerprint the user".

+

Mitigating the risk

+

When evaluating a library for production usage, we ask a few questions to +understand the need for the library in the first place.

+

Can we build this functionality in-house?

+

In some cases we were able to simply copy/paste the parts of a library we really +needed. In more complex scenarios, where a library talked to a custom backend we +reverse-engineered that API and built a mini-SDK ourselves (again, only the +parts we needed). This is the preferred option 90% of the time, but isn't always +feasible when integrating with very specific vendors or requirements.

+

How many customers benefit from this library?

+

In one scenario, we were considering adding a very risky library (according to +the criteria below) intended for a tiny subset of users while still exposing all +of our users to the library. We ran the risk of something going wrong for all +our customers in all our markets for a small group of customers we thought would +benefit from it.

+

What transitive dependencies does this library have?

+

We'll want to evaluate the criteria below for all dependencies of the library as +well.

+

What are the exit criteria?

+

If integration is successful, is there a path to moving it in-house? If it isn't +successful, is there a path to removal?

+

Evaluation criteria

+

If at this point the team still wants to integrate the library, we ask them to +ā€œscoreā€ the library according to a standard set of criteria. The list below is +not comprehensive but should give a good indication of the things we look at.

+

Blocking criteria

+

These criteria will prevent us from including the library altogether, either +technically or by company policy, and need to be resolved before we can move +forward:

+ +

Major concerns

+ +

We assign point values to all these (and a few others) criteria and ask +engineers to tally those up for the library they want to include. While low +scores aren't hard-rejected by default, we often ask for more justification to +move forward.

+

Final notes

+

While this process may seem very strict and the potential risk hypothetical in +many cases, we have actual, real examples of every scenario I described in this +blog post. Having the evaluations written down and publicly available also helps +in conveying relative risk to people unfamiliar with how mobile platforms works +and demonstrating we're not arbitrarily evaluating the risks.

+

Also, I don't want to claim every third-party library is inherently bad. We +actually use quite a few at Lyft: RxSwift and RxJava, Bugsnag's SDK, Google +Maps, Tensorflow, and a few smaller ones for very specific use cases. But all of +these are either well-vetted, or we've decided the risk is worth the benefit +while actually having a clear idea of what those risks and benefits really are.

+

Lastly, as a developer pro-tip: always create your own abstractions on top of +the library's APIs and never call their APIs directly. This makes it much easier +to swap (or remove) underlying libraries in the future, again mitigating some +risk associated with long-term development.

+
+
+ + + \ No newline at end of file diff --git a/2022/09/28/human-factors-in-choosing-technologies/index.html b/2022/09/28/human-factors-in-choosing-technologies/index.html new file mode 100644 index 0000000..ceb6a6b --- /dev/null +++ b/2022/09/28/human-factors-in-choosing-technologies/index.html @@ -0,0 +1,123 @@ + + + + Human factors in choosing technologies - Scott Berrevoets + + + + + + + + + + + +
+
+

Human factors in choosing technologies

+ + +
+
+

I recently saw a thread where someone wanted to introduce a more capable +architecture pattern than what most apps start out with in a small team, but +received some pushback from teammates and was looking for help in countering +their arguments.

+

The thread for the most part focused on the technical benefits of the proposed +pattern, such as testability, separation of concerns, modularity, etc. These are +all valid trade-offs to consider: weā€™ve done the same at Lyft when figuring out +what architectures would suit us best. This is also not unique to architectures, +any big technology that influences the overall direction of the codebase: +SwiftUI vs. UIKit, RxSwift vs. Combine vs. async/await, etc.

+

But over time Iā€™ve realized that even with a technically ā€œperfectā€ solution (a +real oxymoron!), there is an entirely different yet equally important factor to +consider: who are the people using itā€”now and in the future? The success of a +particular technology is highly dependent on the answer to that question, and it +plays a huge role at different stages of the development of that technology.

+

In the thread I mentioned above there was very little focus on the human aspect +of the proposal, so I wanted to list a number of things that I personally ask +myself and our teams before considering moving forward:

+

How much is the onboarding/ramp-up cost? While I generally think short-term +pain is worth long-term gain, onboarding is often a continuous cost. New people +join, you might have interns, non-mobile engineers wanting to make quick +contributions, etc. If those people first need a lot of time to ramp up, itā€™s +worth wondering if the benefits are worth it, or how to reduce that burden. For +example, while we currently aren't using any SwiftUI at Lyft, we have a layer of +sugar syntax on top of UIKit that enables us to use SwiftUI-like syntax anyway. +This makes it easier for both new people that join and already know SwiftUI and +for everybody to move over to SwiftUI if/when we're ready for that.

+

How easy is it to undo? Or: what is the cost of mistakes? If things donā€™t +pan out the way we want them to, how easily could we switch to something else? +The more difficult switching back is, the higher the commitment level and the +more we need to be sure itā€™s worth it. This applies both to the internals of the +framework and how the code that uses the framework is structured.

+

Is it easy to do the right thing? This one is straightforward: if itā€™s easy +to do the right thing itā€™s more likely people will do the right thing and +achieve the architectureā€™s potential more. Conversely, if itā€™s easy to do the +wrong thing, the benefits arenā€™t realized as much. Especially considering my +previous point, if itā€™s hard to undo bad usage itā€™s maybe worth going back to +the drawing board.

+

How much support is available? Popular technologies have a lot of online +material available for support in the form of Stack Overflow questions and +answers, blog posts, videos, open source code + discussions on GitHub, etc. A +home-built solution means this knowledge only lives in-house which increases the +bus factor. The same is true for very opinionated third-party libraries like +RxSwift or The Composable Architecture. Iā€™m a fan of both, but without fully +understanding how theyā€™re implemented youā€™re at the mercy of the developers and +contributors of these libraries for years to come.

+

How much institutional knowledge does it require? Good architectures hide +domain complexity for its consumers, and incur the complexity internally. To +some extent thatā€™s fine, but if the internals become so complicated that few +people know how it works there is again a high bus factor. It can absolutely be +worth putting some complexity/boilerplate burden onto feature owners to avoid +making complex abstractions that are hard to change in the future once itā€™s used +everywhere and the original developers have left.

+

How much effort does it take to see 100% adoption? Depending on the size of +the existing code base, it could take a long time to get 100% adoption. That can +be OK if this is the codebaseā€™s first serious architecture, but if itā€™s version +5 and some parts of the codebase still use version 1 through 3, itā€™s probably +worth removing those first and reducing lava layers. Even if the change +from version 10 to 11 is small and easy, the fragmentation of the codebase +inhibits developer productivity. The quicker the migration the better, and if +the codebase can safely be migrated through automation thatā€™s the best case +outcome.

+

But the most important one of all: do people actually like the architecture? +No one likes working in a codebase where everything is a hassle, the underlying +concepts never seem to make sense, abstractions are leaky, and you seem to +always have to do work for it instead of it working for you. Those codebases +diminish the teamā€™s motivation levels and will affect many of the other points +from above.

+

On the flip side, if people like the proposed patterns, they will put in a lot +more work in to use them correctly, try harder to do the right thing, are +willing to help others, etc. If not, forcing patterns people donā€™t like could +lead to developer unhappiness and attrition. We have more than a few examples of +this at Lyft, where a slightly inferior technical solution is overall much more +beneficial because the pattern is a bit simpler to use than the alternative.

+

Going back to why I started writing this in the first place: in my opinion the +question ā€œwhat counterarguments can I useā€ is not a great first question to ask +when it comes to convincing people your solution is the best one out there. +Understanding why people are resistant is key. Sure, some people just donā€™t +like change, but papering over any of the concerns above with a technically +superior solution is a recipe for a bunch of barely-adopted technologies in a +codebase thatā€™s often worse off than if nothing had been done in the first +place.

+
+
+ + + \ No newline at end of file diff --git a/2022/11/15/migration-strategies-in-large-codebases/index.html b/2022/11/15/migration-strategies-in-large-codebases/index.html new file mode 100644 index 0000000..3ace0e0 --- /dev/null +++ b/2022/11/15/migration-strategies-in-large-codebases/index.html @@ -0,0 +1,187 @@ + + + + Migration strategies in large codebases - Scott Berrevoets + + + + + + + + + + + +
+
+

Migration strategies in large codebases

+ + +
+
+

Code migrations are a fact of life for large codebases. New technologies pop up, +platforms see improvements, and programming languages get new features you might +want to adopt. Not performing these migrations and keeping up with the times is +simply not an option in most cases. While some migrations are easy 1 for 1 +replacements that can/should be scripted, thatā€™s not true for the more +ā€œsemanticā€ migrationsā€”the new code accomplishes the same but in an entirely +different manner.

+

Weā€™ve done a number of these semantic migrations in Lyftā€™s mobile codebases and +while weā€™ve learned something from every one of them (and applied the learnings +the next time), they only get harder as time goes on.

+

What makes migrations so difficult? This generally comes down to how development +works in large codebases: there is no 1 mobile team, but rather a lot of +vertical teams (e.g. payments, new user experience, etc.) that all build their +own features, using shared foundational code and architectural patterns. The +coordination of dozens of teams on the progress of a migration is what makes +this difficult:

+ +

So how do we get these migrations done anyway? There is no silver bullet, but we +have started using a number of strategies to make these migrations less +painfulā€”and almost all of them have to do with improving the communication and +expectations everyone involved has.

+

Migration tracker

+

The biggest leap forward for us was the rollout of an in-house web dashboard +that shows progress on all ongoing migrations in our iOS and Android codebases. +It shows all the important information anyone could want to know: the start +date, (projected) completion date, links to documentation and support channels, +relative priority (more on that below), etc.

+

But the real win is automatic progress tracking. Every migration is defined as +the presence of a certain pattern in the codebase we want to get rid of. A cron +job records all instances of that pattern on the main branch once a day, so we +know exactly how much code has been migrated and how much is left.

+

Furthermore, our codebase is heavily modularized, and through CODEOWNERS every +module is required to define what team owns it. This data is shared with the +Migration Tracker, so it can give a detailed breakdown by module and by team +of how much unmigrated code is left. This is extremely helpful in understanding +which teams might need extra time, help, or nudging in getting everything done.

+

Timeline

+

The ā€œdeadlineā€ of a migration needs to be carefully chosen. Not giving people +enough time makes them not do the work at all, giving them too much time means +people will procrastinate and needlessly extend the migration time. If it looks +like teams are not going to make the deadline (as projected by the Migration +Tracker), we ask them when they can get it done. We usually accept whatever +answer the team gives, but hold them accountable to that since they themselves +picked that timeframe.

+

Priorities

+

Teams that are far behind in code modernization might have to migrate their code +in multiple ways. We give each migration a priority (P0/P1/P2/P3) to give teams +a sense of where to focus their tech debt energy first.

+

A P0 migration is a ā€œmust-do-or-elseā€ (externally mandated changes, tight +deadlines, extremely high impact on overall trajectory of the codebase or +product, etc.), a P1 is high impact but not as urgent, P2 is a nice to have, and +P3 is almost more of an FYI.

+

This helps everyone understand where they should focus their energy, instead of +seeing a potentially long list of items that are all seemingly equally important +migrations.

+

Incremental value

+

Where possible (which is most cases), we try to bring incremental value as a +migration is being performed. For example, if we set out to replace all uses of +class A with class B because class B performs better, the places where class B +is used should see that performance increase before the rest of the code has +been migrated. This has a few benefits:

+ +

Only invest in the new

+

The initial value (like performance improvements in the example above) teams get +from migrating their own code might not be enough to convince them to do it. But +as more and more improvements are made to ā€œclass Bā€ (e.g. API ergonomics, +integrated analytics, compatibility with other systems) that ā€œclass Aā€ doesnā€™t +have, the value proposition for the migration changes with time. We no longer +update A, which means there's some amount of bit rot and it becomes more +cumbersome to use.

+

In extreme cases we could even remove features of class A that would necessitate +a migration, but we havenā€™t done that so far and would like to avoid it to avoid +losing goodwill with teams.

+

Develop great partnerships

+

Getting adoption of a new pattern or tool is difficult even if it doesnā€™t +involve a new migration, because there is no precedent. Early adopters will hit +all the rough edges and have often incomplete documentation and no sample code. +The developer experience isnā€™t great (yet), so we make ourselves very available +to their needs. We take their feedback seriously and address it promptly, sit +down with them to help solve their issues, and generally spend a lot of time +with them working out the kinks. We do this 1) to build more confidence that +what we built is production-ready and 2) to recognize/reward people that want to +try new things by reducing as much of the growing pains as possible.

+

Of course, good developer experience is important for all teams and not just +early adopters, but piloting teams can help us get the ball rolling with other +teams as well. Positive reviews from users of Great New Thing is different than +hearing it from the developers of Great New Thing.

+

Avoid new uses

+

To avoid creating a moving target, as soon as we start a new migration we lock +down the uses of that pattern to only the current ones and no longer accept new +uses of this pattern. Depending on the situation we have a few different ways of +accomplishing this:

+ +

These measures are just to generate awareness that a pattern is outdated, it +doesnā€™t always communicate why itā€™s being deprecated and whatā€™s replacing it. +But if people come to us asking for an exception we can explain it or link to +docs, an opportunity we wouldnā€™t have had if we didnā€™t do any of the things +above.

+

Support, support, support

+

And that brings me to the most critical and time-consuming piece: support. This +means helping people with questions, plugging gaps new patterns have compared to +the old ones, checking in with people on progress, celebrating the completion of +major (or all!) migrations with everyone that helped contribute, assisting teams +that are having difficulty prioritizing the work, and anything else that gets +the work done.

+

Migrations are a very specific type of codebase maintenance due to the large +footprint they often have in a lot of the codebase. Itā€™s an all-hands on deck +situation and requires proper coordination and communication, sometimes for +years on end. Itā€™s hard to get them right and weā€™re still fine-tuning our +strategies but applying the learnings I described here have made us become +better at them. I can only hope one day refactoring tools become smart enough to +automate the work for us.

+
+
+ + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..b52fb41 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +www.scottberrevoets.com diff --git a/about.html b/about.html new file mode 100644 index 0000000..2b9fcfa --- /dev/null +++ b/about.html @@ -0,0 +1,49 @@ + + + + About - Scott Berrevoets + + + + + + + + +
+
+

+
+ +
+

Hi, I'm Scott! Back in 2014 I started as one of Lyft's first iOS hires, and I +still work there now. I've seen and helped the apps grow from a monolithic +Objective-C codebase to a highly sophisticated class of apps and underlying +technologies.

+

I'm currently on Lyft's Mobile Infra team, where I look after the quality and +scalability of our iOS and Android codebases.

+

Some my other extracurriculars include:

+ +

For a full list of my work, check out my resume [PDF | +HTML.

+
+
+ + \ No newline at end of file diff --git a/archives.html b/archives.html new file mode 100644 index 0000000..15dc1a4 --- /dev/null +++ b/archives.html @@ -0,0 +1,60 @@ + + + + Scott Berrevoets - Archives + + + + + + + + +

Archives for Scott Berrevoets

+ +
+
15 November 2022
+
Migration strategies in large codebases
+
28 September 2022
+
Human factors in choosing technologies
+
15 July 2022
+
Third-party libraries are no party at all
+
14 October 2021
+
iOS Architecture at Lyft
+
08 August 2018
+
Re-binding self: the debugger's break(ing) point
+
06 March 2017
+
Using Interface Builder at Lyft
+
01 August 2016
+
Silencing NSLog
+
21 March 2016
+
Outlets: strong! or weak?
+
23 July 2015
+
@objc class prefixes fixed in Xcode 7 beta 4
+
13 June 2015
+
The power of UIStackView
+
15 December 2014
+
@objc creates a wrong class name in Objective-C
+
20 October 2014
+
Dev-only iPhones
+
15 September 2014
+
UIMenuController and the responder chain
+
10 August 2014
+
Xcode & Objective-C Oddities
+
25 July 2014
+
Objective-C prefixes: a thing of the past?
+
+ + \ No newline at end of file diff --git a/authors.html b/authors.html new file mode 100644 index 0000000..69e8aaa --- /dev/null +++ b/authors.html @@ -0,0 +1,30 @@ + + + + Scott Berrevoets - Authors + + + + + + + + +

Authors on Scott Berrevoets

+ + + \ No newline at end of file diff --git a/categories.html b/categories.html new file mode 100644 index 0000000..12ac174 --- /dev/null +++ b/categories.html @@ -0,0 +1,30 @@ + + + + Scott Berrevoets - Categories + + + + + + + + +

Categories on Scott Berrevoets

+ + + \ No newline at end of file diff --git a/images/UIStackView.png b/images/UIStackView.png new file mode 100644 index 0000000..1f89c6f Binary files /dev/null and b/images/UIStackView.png differ diff --git a/images/XcodeScreenshot.png b/images/XcodeScreenshot.png new file mode 100644 index 0000000..311479c Binary files /dev/null and b/images/XcodeScreenshot.png differ diff --git a/images/flows.png b/images/flows.png new file mode 100644 index 0000000..cd2cbd6 Binary files /dev/null and b/images/flows.png differ diff --git a/images/graph.png b/images/graph.png new file mode 100644 index 0000000..a08c96f Binary files /dev/null and b/images/graph.png differ diff --git a/images/me.jpg b/images/me.jpg new file mode 100644 index 0000000..12827ed Binary files /dev/null and b/images/me.jpg differ diff --git a/images/plugins.png b/images/plugins.png new file mode 100644 index 0000000..a2a6b05 Binary files /dev/null and b/images/plugins.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..20e1217 --- /dev/null +++ b/index.html @@ -0,0 +1,143 @@ + + + + Scott Berrevoets + + + + + + + + + +
+
+
2022
+
+ + + Migration strategies in large codebases + +
+
+ + + Human factors in choosing technologies + +
+
+ + + Third-party libraries are no party at all + +
+
2021
+
+ + + iOS Architecture at Lyft + +
+
2018
+
+ + + Re-binding self: the debugger's break(ing) point + +
+
2017
+
+ + + Using Interface Builder at Lyft + +
+
2016
+
+ + + Silencing NSLog + +
+
+ + + Outlets: strong! or weak? + +
+
2015
+
+ + + @objc class prefixes fixed in Xcode 7 beta 4 + +
+
+ + + The power of UIStackView + +
+
2014
+
+ + + @objc creates a wrong class name in Objective-C + +
+
+ + + Dev-only iPhones + +
+
+ + + UIMenuController and the responder chain + +
+
+ + + Xcode & Objective-C Oddities + +
+
+ + + Objective-C prefixes: a thing of the past? + +
+
+
+ + \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 0000000..9339852 --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,143 @@ + + + + Scott Berrevoets + + + + + + + + + +
+
+
2022
+
+ + + Migration strategies in large codebases + +
+
+ + + Human factors in choosing technologies + +
+
+ + + Third-party libraries are no party at all + +
+
2021
+
+ + + iOS Architecture at Lyft + +
+
2018
+
+ + + Re-binding self: the debugger's break(ing) point + +
+
2017
+
+ + + Using Interface Builder at Lyft + +
+
2016
+
+ + + Silencing NSLog + +
+
+ + + Outlets: strong! or weak? + +
+
2015
+
+ + + @objc class prefixes fixed in Xcode 7 beta 4 + +
+
+ + + The power of UIStackView + +
+
2014
+
+ + + @objc creates a wrong class name in Objective-C + +
+
+ + + Dev-only iPhones + +
+
+ + + UIMenuController and the responder chain + +
+
+ + + Xcode & Objective-C Oddities + +
+
+ + + Objective-C prefixes: a thing of the past? + +
+
+
+ + \ No newline at end of file diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 0000000..9339852 --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,143 @@ + + + + Scott Berrevoets + + + + + + + + + +
+
+
2022
+
+ + + Migration strategies in large codebases + +
+
+ + + Human factors in choosing technologies + +
+
+ + + Third-party libraries are no party at all + +
+
2021
+
+ + + iOS Architecture at Lyft + +
+
2018
+
+ + + Re-binding self: the debugger's break(ing) point + +
+
2017
+
+ + + Using Interface Builder at Lyft + +
+
2016
+
+ + + Silencing NSLog + +
+
+ + + Outlets: strong! or weak? + +
+
2015
+
+ + + @objc class prefixes fixed in Xcode 7 beta 4 + +
+
+ + + The power of UIStackView + +
+
2014
+
+ + + @objc creates a wrong class name in Objective-C + +
+
+ + + Dev-only iPhones + +
+
+ + + UIMenuController and the responder chain + +
+
+ + + Xcode & Objective-C Oddities + +
+
+ + + Objective-C prefixes: a thing of the past? + +
+
+
+ + \ No newline at end of file diff --git a/resume.pdf b/resume.pdf new file mode 100644 index 0000000..0cf89c6 Binary files /dev/null and b/resume.pdf differ diff --git a/tags.html b/tags.html new file mode 100644 index 0000000..007b709 --- /dev/null +++ b/tags.html @@ -0,0 +1,29 @@ + + + + Scott Berrevoets - Tags + + + + + + + + +

Tags for Scott Berrevoets

+ + + \ No newline at end of file diff --git a/theme/css/pygments.css b/theme/css/pygments.css new file mode 100644 index 0000000..6e3bdc7 --- /dev/null +++ b/theme/css/pygments.css @@ -0,0 +1,76 @@ +/* + Darkly Pygments Theme + (c) 2014 Sourcey + http://sourcey.com +*/ + +pre, code { + font-family: 'SFMono-Regular', 'SF Mono', Menlo, monospace; + font-size: 0.8em; + color: #555; +} + +.highlighttable { + border-radius: 5px; + border-collapse: collapse; + border-spacing: 0; + width: 100%; +} + +.highlighttable .linenos { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + background-color: #C1C2C3; + padding: 0 5px; + width: 1%; + text-align: right; +} + +.highlighttable .code { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + background: #343642; + position: relative; +} + +.highlighttable .highlight { + overflow-x: auto; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +code { + overflow: auto; + max-width: 100%; + display: inline; + font-size: 1em; +} + +.highlighttable .highlight pre { + color: #C1C2C3; + padding-left: 10px; +} + +.highlight .hll { background-color: #ffc; } +.highlight .gd { color: #2e3436; background-color: #0e1416; } +.highlight .gr { color: #eeeeec; background-color: #c00; } +.highlight .gi { color: #babdb6; background-color: #1f2b2d; } +.highlight .go { color: #2c3032; background-color: #2c3032; } +.highlight .kt { color: #e3e7df; } +.highlight .ni { color: #888a85; } +.highlight .c,.highlight .cm,.highlight .c1,.highlight .cs { color: #8D9684; } +.highlight .err,.highlight .g,.highlight .l,.highlight .n,.highlight .x,.highlight .p,.highlight .ge, +.highlight .gp,.highlight .gs,.highlight .gt,.highlight .ld,.highlight .s,.highlight .nc,.highlight .nd, +.highlight .ne,.highlight .nl,.highlight .nn,.highlight .nx,.highlight .py,.highlight .ow,.highlight .w,.highlight .sb, +.highlight .sc,.highlight .sd,.highlight .s2,.highlight .se,.highlight .sh,.highlight .si,.highlight .sx,.highlight .sr, +.highlight .s1,.highlight .ss,.highlight .bp { color: #C1C2C3; } +.highlight .k,.highlight .kc,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr, +.highlight .nt { color: #729fcf; } +.highlight .cp,.highlight .gh,.highlight .gu,.highlight .na,.highlight .nf { color: #E9A94B ; } +.highlight .m,.highlight .nb,.highlight .no,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo, +.highlight .il { color: #8ae234; } +.highlight .o { color: #989DAA; } +.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi { color: #fff; } diff --git a/theme/css/style.css b/theme/css/style.css new file mode 100644 index 0000000..aaf6501 --- /dev/null +++ b/theme/css/style.css @@ -0,0 +1,221 @@ +@import url("pygments.css"); + +body { + color: rgb(50, 50, 50); + margin: 0 auto; + width: 100%; + max-width: 800px; + padding: 0; + font-size: 1.15em; + line-height: 1.5em; +} + +body > header { + margin: 0; + border-top: 5px solid rgb(203, 18, 22); +} + +a { + color: inherit; +} + +a:hover { + color: rgb(203, 18, 22); +} + +ul#social { + display: table; + margin: 10px auto; + width: 200px; + -webkit-padding-start: 0; +} + +ul#social li { + list-style-type: none; + display: table-cell; + text-align: center; +} + +h1, h2 { + font-family: -apple-system-headline, 'SFProDisplay-Bold', -apple-system, system-ui, BlinkMacSystemFont; +} + +h1 { + text-align: center; + margin-top: 40px; +} + +h1 a { + color: rgb(203, 18, 22); + text-decoration: none; + font-size: 2em; +} + +@media only screen and (max-width: 500px) { + h1 a { + font-size: 1.2em; + } +} + +nav#menu { + font-family: 'SFProText-Bold', -apple-system, system-ui, BlinkMacSystemFont; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + width: 70%; + min-width: 300px; + margin: 0 auto; +} + +nav#menu:after { + content: ""; + clear: both; + display: table; +} + +nav#menu ul { + display: table; + table-layout: fixed; + width: 100%; + -webkit-padding-start: 0; +} + +nav#menu ul li { + display: table-cell; + width: auto; + list-style-type: none; + text-align: center; +} + +nav#menu ul li a { + text-transform: uppercase; + text-decoration: none; + font-weight: bold; + color: rgb(180, 180, 180); +} + +nav#menu ul li.active a { + color: rgb(50, 50, 50); +} + +h2.article-title { + font-size: 2.5em; + padding: 0 10px; + line-height: 1em; + margin-bottom: 0; +} + +div.article-content, div.page-content { + line-height: 1.5em; + font-family: -apple-system-body, -apple-system, system-ui, BlinkMacSystemFont; + padding: 10px; +} + +div.pagination { + text-align: center; +} + +div.pagination ul { + width: auto; + display: inline-block; + list-style-type: none; + -webkit-padding-start: 0; +} + +div.pagination ul li { + display: inline; + font-family: -apple-system, system-ui, BlinkMacSystemFont; +} + +div.pagination ul li.prev { + margin-right: 20px; +} + +div.pagination ul li.next { + margin-left: 20px; +} + +/*** About ***/ + +.left-column { + float: left; + min-width: 25%; +} + +.right-column { + float: left; + width: 75%; +} + +img#me { + display: inline-block; + margin-right: 1.2em; + border-radius: 18px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + border: 0; +} + +/*** Archive ***/ + +dl#archive { + display: table; +} + +dl#archive dt { + font-size: 1.3em; + color: #aaa; +} + +dl#archive dd { + margin: 20px 0 20px 40px; +} + + +@media only screen and (max-width: 320px) { + dl#archive dd { + margin-left: 0; + } +} + +dl#archive dt + dd { + margin-top: 0; +} + +dl#archive dd time { + text-transform: uppercase; + color: #aaa; + font-weight: 600; + font-size: 0.9em; + vertical-align: text-top; + display: table-cell; + width: 100px; +} + +dl#archive dd a { + font-size: 1.3em; + text-decoration: none; + line-height: 1.3em; + display: table-cell; +} + +/*** Article ***/ + +header time.published { + padding: 0 10px; + font-family: -apple-system-subheadline, 'SFProDisplay-Bold', -apple-system, system-ui, BlinkMacSystemFont; + color: #aaa; +} + +blockquote { + width: 85%; + margin: 30px auto; + padding-left: 1.5em; + border-left: 4px solid rgb(221, 221, 221); + color: #777; +} + +div#disqus_thread { + margin-top: 50px; + padding: 10px; +} diff --git a/theme/images/github.svg b/theme/images/github.svg new file mode 100644 index 0000000..2d13645 --- /dev/null +++ b/theme/images/github.svg @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/theme/images/linkedin.svg b/theme/images/linkedin.svg new file mode 100644 index 0000000..59cc577 --- /dev/null +++ b/theme/images/linkedin.svg @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/theme/images/mail.svg b/theme/images/mail.svg new file mode 100644 index 0000000..561d0e3 --- /dev/null +++ b/theme/images/mail.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/theme/images/mastodon.svg b/theme/images/mastodon.svg new file mode 100644 index 0000000..4883225 --- /dev/null +++ b/theme/images/mastodon.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file