Coder Social home page Coder Social logo

blog's People

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

iOS crash symbolication script

A script to symbolicate crash log and verify if the crash log file matches the build UUID

To get the best symbolication result, or event to get a proper symbolication of a crash log, the appropriate dSYM and application build is required.
The symbolicatecrash tool allows to symbolicate a crash log that doesn't exactly match the build and dSYM that you would like to use.
This script besides the symbolication, it also checks if the crash log UUID matches the build UUID.
Just run this script on a folder with your .ipa, the corresponding .dSYM, and (1+) .crash files. Will output symbolicated sym-*.crashes for you.

Download the script

#!/bin/bash
#
# Fool'n'Lazy-Proof iOS .crash Symbolication
#
# Just run this script on a folder with your `.ipa`, the corresponding `.dSYM`, 
# and (1+) `.crash` files. Will output symbolicated `sym-*.crash`es for you.
#
# Copyright (c) 2016 Ferran Poveda (@fbeeper)
# Provided under MIT License (MIT): http://choosealicense.com/licenses/mit/
#
# Extended by Lukasz Sliwinski to check equality of the build's UUID with the crash logs
#

function checkForRequiredFileOfType() 
{ 
	count=`find $1 -print -quit 2> /dev/null | wc -l | awk '{print \$1}'`
	if [[ $count > 0 ]]; then
		echo "Found a $1 file"
	else
		echo "Missing a $1 file! Need *.ipa + *.dSYM + *.crash files."
		exit
	fi
}

function checkForRequiredFiles()
{
	checkForRequiredFileOfType "*.ipa"
	checkForRequiredFileOfType "*.dSYM"
	checkForRequiredFileOfType "*.crash"
}

# Define location of symbolicatecrash binary (defaults to Xcode location, but can be defined on params)
symbolicatecrash=${symbolicatecrash:-/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash}

# Let's make sure you haven't forgotten any file
checkForRequiredFiles

# Extract .ipa and define where the binary is (assuming it has the same name)
ipa=`find *.ipa -print -quit`
bsdtar -xf "$ipa" -s'|[^/]*/||'
app=`find *.app -print -quit`
app="$app$/${app%.*}"

dSYM=`find *.dSYM -print -quit`
uuid=`dwarfdump -u $dSYM | perl -ne 'print $1 if /UUID: (.*) \(arm64\)/s' | cut -c 1-42 | tr -d '-' | awk '{ print tolower($0) }'`

echo ""
echo "Build UUDD: $uuid"
echo ""

# Symbolicate all .crash files
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
for i in *.crash; do

	if [[ ! $i == sym* ]]; then # Skips previous output files from this script

		build_name=`grep '{"app_name":' $i | perl -ne 'print $1 if /{"app_name":"([^"]+)","/s' | awk '{ print($0) }'`

		crashFileUuid=`grep --after-context=1000 "Binary Images:" $i | grep "$build_name arm64" | perl -ne 'print $1 if /.*<(.*)>/s' | awk '{ print($0) }'`

		if [ "$uuid" == "$crashFileUuid" ]; then
			"$symbolicatecrash" "$i" "$app" > "sym-$i"
			echo "✅ Symbolicated $i"
		else
			echo "❌ skipped $i (crash UUID [$crashFileUuid] do not match build's UUID: [$uuid]"
		fi

	fi

done

Setup a linkage and package for each dependency separately with CocoaPods

How to set up the linkage and package method separately for each dependency with CocoaPods

CocoaPods doesn't allow the setup of the linkage and package method separately for each dependency.
It is possible to setup the linkage method but only for the whole target like:

target :DynamicTarget do
    use_frameworks! :linkage => :dynamic
    
    pod 'DynamicPod' # This will be linked dynamically
end

target :StaticTarget do
    use_frameworks! :linkage => :static

    pod 'StaticPod' # This will be linked statically
end

Microsoft iOS team made a plugin to allow it:
https://github.com/microsoft/cocoapods-pod-linkage

If you don't what to use the plugin you can still make it work on your own by using the CocoaPods pre_install hook.

pre_install do |installer|
  installer.pod_targets.each do |pod|
    if pod.name == "Podname"
      def pod.build_type
        Pod::BuildType.new(:linkage => :dynamic, :packaging => :framework)
      end
    end
  end
end

All possible BuildTypes are defined here https://github.com/CocoaPods/Core/blob/master/lib/cocoapods-core/build_type.rb.

Detecting a gate's gearmotor open/closed state without reed switch with ESP32/8266 & ESPHome.

How to detect a gate's gearmotor open/closed state using 24 V AC/DC accessories power-supply output with a microcontroller?

Most of the gate gearmotor has 24 V AC/DC accessories power-supply output. Useful to connect signaling devices (an external lamp) which is turned on while the gate is open. I wanted to use that information to be able to know if the gate is open or closed in my home automation system.
I could use reed switches as I do for the garage door opener. But the priority was to pack everything inside the gearmotor cover because it is on the outside. I wanted to use ESP32/8266 microcontroller, the main problem for me to solve was, how to convert 24 V AC/DC voltage to something that I can connect to GPIO pin, which in the case of ESP can be 0 - 3.3 V.

Optocouplers

I knew that optocouplers can be useful for this, but I wasn't sure how to properly make a circuit that would be false-positive protected. Fortunately, I have found an outstanding article that explains in detail exactly what I needed. I just had to adjust a bit the elements for the 24 V instead of 120/230 V. My gate gearmotor has 24 AC Voltage but the circuit should work also for the DC case.
The secondary side of the optocoupler is galvanically separated from mains electricity (inside the optocoupler is LED diode). The diode lights up when the input voltage exceeds around 1.4 V (I used LTV-814/PC817). When the diode lights up the built-in transistor works in the active region and acts as a short circuit. When the output voltage is below 1.4 V (when the gate is closed we expect the voltage to be 0) the transistor operates in the cutoff region and acts as an open circuit.
This way we get a relay behavior that can safely connect almost any microcontroller to the output.

I specifically picked the LTV-814 bidirectional optocoupler. It has two LEDs conducting in opposite directions. It simplifies the circuit a bit because thanks to the second LED the effect on the secondary side voltage (AC) will be very similar to the use of a rectifier. The last optional thing but very useful is to add a capacitor between the Emitter and GND. It smooths out the flow which helps to protect from getting too much noise information especially when the input is switching between the 0 - 24 V.
The circuit that I picked from the mentioned article:

image

I just changed the resistors from 100k to 2k. Instead of using 1 resistor that could have 6k its a good idea to use a couple of resistors in series as mentioned in the article.

In this way, the power is distributed over several components instead of one. Secondly, and even more important is the maximum voltage that the resistor can take. We have done the detecting part of the AC/DC voltage from the gate gearmotor.

Connecting the voltage detector

Having the voltage detector, I had to connect it between the gearmotor's terminals for signaling devices and the microcontroller's GPIO. I have the CAME BXV series SDNX gearmotor. Using the manual I just had to find where are the connectors for the signaling devices. In my case, it was very easy to access, just unscrue one scrue to take off the cover, and then already on the board ⑫ the terminals ⑱ are visible.
Because in my case the ESP8266 will be placed under the gearmotor cover I also could easily add another feature. Using the relay controller by the microcontroller I can short circuit for a short time that is connected to the command terminals ⓸. I already used those to be able open/close my gate from home, by using the wall-mounted momentary-action switch.

Gearmotor Board
gearmotor board

The whole circuit:
circuit

I placed everything inside a box and it fits nicely inside the gearmotor cover:

Elements Placement
photo_circuit_description photo_box_in_cover

Programming

Having that done, the last piece was to program the ESP8266. I used ESPHome.
Basically, we have to react to the voltage detection that gives us information if the gate is open or closed (and also when the state is changing). In addition, we can open/close the gate by using the relay.
There is one caveat though. We do not if the gate is fully open, and we do not know if the opening ended. At least, I couldn't find in my gearmotor easy access to such information. I could add a reed switch to detect the fully opened state, but I decided that this information is not that important for me.
Knowing that I do not have precise open position feedback I implemented time-based cover opening/closing as an alternative and optional feature. The fully open state of the cover is thus always an assumed one, the current position is approximated with the time the cover has been moving in a direction.

Please see the component file to check the full component implementation. I won't describe every detail of it. Its the best to check the code itself to grasp how it works. It might be also interesting for you to check out the gate.yaml file to see how to use and set up the component.

The important bit is that you can configure a few parameters used in the gate component:

  # gate
  gate_duration: 19500ms
  gate_duration_int: "19000"

  gate_close_pin: GPIO14
  gate_close_debounce_time: 500ms

  gate_relay_pin: GPIO4
  gate_relay_active_duration: 400ms
  • gate_duration and gate_duration_int - configures how long it takes to fully open/close the gate. In my case, it is around 19 seconds. To parameters are required because of the ESPHome limitation to pass the integer information to the component.
  • gate_close_pin - configures which GPIO pin will be used to detect the voltage from the gearmotor.
  • gate_close_debounce_time - configures the debounce time that can programmatically protect from false positives while the state of the voltage detection is changing. 500ms should be a reasonable default but you can adjust as needed.
  • gate_relay_pin - configures which GPIO pin will take care of sending a signal to the relay to switch it on and off to a short circuit that will open/close the gate.
  • gate_relay_active_duration - configures how long the relay should short circuit to start the close/open action. Probably anything between 250 - 500ms should be fine.

I have all of my IoT devices connected to the open-source home automation Home Assistant. Which allows adding many other features. I have configured push notifications to not forget to close the gate if I left it open for too long. Also, the setup is configured with HomeKit, so Hey Siri, close the gate works nicely and my kids have a lot of fun too while we talk to her to do our home automation :).

This is how it works in action. The first video presents the closing of the gate initiated from Home Assistant (time-based percentage position is changing). The second one shows received notifications about the open/close states (the gate was controlled from a remote).

Home Assistant Open/Close state
Video 1 Video 2

Unit tests with several localizations

How to make unit tests with several localizations

Since Xcode 11 there is a possibility to configure different locales for unit tests by using Test Plans. It is fine, but a little bit cumbersome as requires the Xcode-specific configuration.

Another way of making it work is the old-school method swizzling.
It is quite simple to do and does its job with quite nice flexibility. It doesn't require any Xcode setup, works well with SPM too.
The idea could be also extended to customize not only the Locale, but also e.g. preferredlanguages.
The key point is that we can do it by exchanging the methods on NSLocale instead of Locale which still is used as a wrapper over the objective-c predecessor.

We can use method_setImplementation and @convention(block) (swift documentation) in this case, which makes it a little bit more ergonomic than defining the @objc method that would solve the purpose of the exchanged method.

extension XCTestCase {
   func setLocale(identifier: String, preferredLanguages: [String]) {
        let currentlLocale: @convention(block) (AnyObject)
            -> AnyObject = { (_: AnyObject!) -> NSLocale in
                return NSLocale(localeIdentifier: identifier)
            }

        method_setImplementation(
            class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.current))!,
            imp_implementationWithBlock(currentlLocale)
        )

        let preferredLanguages: @convention(block) (AnyObject)
            -> [String] = { (_: AnyObject!) -> [String] in
                return preferredLanguages
            }

        method_setImplementation(
            class_getClassMethod(NSLocale.self, #selector(getter: NSLocale.preferredLanguages))!,
            imp_implementationWithBlock(preferredLanguages)
        )
    }
}

Having that we can easily change the Locale for each test. The only downside is that you still use singleton Locale.current, so running tests in parallel will not work reliably.

class Test: XCTestCase {
    func test_locale() {
        setLocale(identifier: "fr", preferredLanguages: ["fr", "de", "pl"])

        XCTAssertEqual(Locale.current, .init(identifier: "fr"))
        XCTAssertEqual(Locale.preferredLanguages, ["fr", "de", "pl"])
    }
}

How to calculate time needed to animate by a given length according to the animation timing function

How to calculate a duration that is needed to animate a view from position A to position B with a given timing function

A long time ago I had a problem to solve that I needed to know the exact time that will take to animate a view by x which might be in the middle of the desired position giving into account the timing function that the animation uses.

Here it is, an objective-c category:

//  CAMediaTimingFunction+Duration.h
#import <QuartzCore/QuartzCore.h>

@interface CAMediaTimingFunction (Duration)

- (NSTimeInterval)timeNeededToMoveByY:(CGFloat)yMove totalYMove:(CGFloat)totalYMove duration:(NSTimeInterval)duration;

@end

#import "CAMediaTimingFunction+Duration.h"

@implementation CAMediaTimingFunction (Duration)

- (NSTimeInterval)timeNeededToMoveBy:(CGFloat)move totalMove:(CGFloat)totalMove duration:(NSTimeInterval)duration {
    // Using reference animation calculate needed time according to used timing function
    CGFloat normalizedMove = move/totalMove;

    // View only just for calculation
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
    CALayer *referenceLayer = view.layer;
    referenceLayer.hidden = YES;
    referenceLayer.speed = 0.0;
    [[UIApplication sharedApplication].keyWindow addSubview:view];

    // Reference animation to calculate time
    CABasicAnimation *basicAnimation =  [CABasicAnimation animationWithKeyPath:@"frame"];
    basicAnimation.duration = 1.0;
    basicAnimation.timingFunction = self;
    basicAnimation.fromValue = [NSValue valueWithCGRect:CGRectMake(0, 0, 1, 1)];
    basicAnimation.toValue = [NSValue valueWithCGRect:CGRectMake(100, 0, 1, 1)];

    [view.layer addAnimation:basicAnimation forKey:@"evaluatorAnimation"];

    // Force to run run-loop to get the presentation layer
    [[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]];

    NSUInteger n = 0;
    CGFloat a = 0.0;
    CGFloat b = 1.0;
    CGFloat tolerance = 0.005;
    CGFloat move = 0.0;
    CGFloat middle = 0.0;

    // Biselection algorithm
    while (n < 1000) {
        middle = (a + b)/2;
        referenceLayer.timeOffset = middle;
        // Refresh animation to get updated presentation layer
        [[NSRunLoop mainRunLoop] runUntilDate:[NSDate date]];
        move = referenceLayer.presentationLayer.position.x / 100;

        if ((move - tolerance) <= normalizedMove && normalizedMove <= (move + tolerance))
            break;

        n += 1;
        if (normalizedMove < move)
            b = middle;
        else
            a = middle;
    }

    [view removeFromSuperview];
    return middle * duration;
}

@end

Tagging in swift documentation to link to another place in the code base

Tag - Swift documentation markup

Swift documentation markup is not clear how to link to other type definition/method/... from the documentation.
It can be done using Tag element.

/// An error
/// - Tag: SomeError
enum SomeError: Error {
}

/// Description
/// - Throws: An error of type [SomeError](x-source-tag://SomeError) might be thrown
func doSomething() throws {
    throw NSError(domain: "", code: 1, userInfo: nil)
}

If you do the quick help (⌥ + click) on the method doSomething the documentation will be presented with the clickable link SomeError that will navigate to the place where the tag is defined.

Screenshot 2021-02-04 at 08 39 47

SwiftUI custom localization strings handling.

How to handle localized strings from a separate bundle or framework in SwiftUI

SwiftUI provides LocalizedStringKey that can be used to initialize several View types, such as Text, Toggle, and others.
It works differently from UIKit in that regard, as if the localized strings leave in some other bundle than the main one from where by default SwiftUI tries to get the values, you have to pass that information to the Text constructor (not to LocalizedStringKey), in contrast to NSLocalizedString.

It is connected to the fact that SwiftUI internally handles the possibility to easily override the locale used by the view.

view.environment(\.locale, .init(identifier: "pl"))

It is very handy, especially in the case of the SwiftUI Previews.

ForEach(localizations, id: \.identifier) { locale in
    Text("Hello")
        .environment(\.locale, locale)
        .previewDisplayName(Locale.current.localizedString(forIdentifier: locale.identifier))
}

The problem is when you would like to use NSLocalizedString, or you have a custom implementation for the localized strings handling. Then most probably you will lose the ability to set the locale by .environment(\.locale, locale) on the views because it won't be respected. As the environment value is available in the View context and Locale.current stays the same across the app. There is no way to get the locale set for the given view and its child views outside of the body.

To overcome that issue I came up with a solution that allows to override of the locale environment and provide a custom implementation of the localized strings.

struct LocalizedText: View {
    @Environment(\.locale) var locale
    let key: String
    let localizedString: (_ languageCode: String?, _ key: String) -> (String)

    var body: Text {
        let languageCode = locale.languageCode ?? Locale.current.languageCode
        let localizedString = localizedString(languageCode, key)

        return Text(localizedString)
    }
}

LocalizedText view is used to get the locale from the environment as it is only available from the View context.
Using localizedString closure, which gets all the information needed to resolve the localized string which is the key and languageCode (if needed the whole Locale object could be passed instead).

Example of use could look like this:

extension L18n {
    static func localizedString(_ key: String, languageCode: String) -> String {
        NSLocalizedString(
            key,
            tableName: "",
            bundle: resolveBundle(for : languageCode),
            value: "**\(key)**",
            comment: ""
        )
    }

    static func localizedText(_ key: String) -> some View {
        LocalizedText(key: key, localizedString: { languageCode, key in
            localizedString(key, languageCode: languageCode)
        })
    }
}
struct SomeView: View {
    var body: some View {
        L18n.localizedText("Hello")
    }
}

struct SomeView_Preview: PreviewProvider {
    static var previews: some View {
        SomeView()
            .environment(\.locale, Locale(identifier: "pl"))
    }
}

This way we keep the best of both worlds. Especially do not lose the ability of SwiftUI EnvironmentValues and still provide a custom implementation for the localized strings handling. We can keep *.lproj and *.strings files in a separate framework, and provide a custom implementation of the localized strings by e.g. having a complicated fallback translations business logic in case the key is missing for the asked language.

Custom Key Binding for Xcode to join lines

How to add join lines shortcut to Xcode

Add the section below to the file IDETextKeyBindingSet.plist that might be located at:
/Applications/Xcode.app/Contents/Frameworks/IDEKit.framework/Versions/A/Resources/IDETextKeyBindingSet.plist
depending where the Xcode app is placed and named.

<key>Custom</key>
<dict>
    <key>Join Lines</key>
    <string>moveDown:, moveToEndOfText:, moveToBeginningOfText:, deleteToBeginningOfLine:, deleteBackward:</string>
    <key>Join Lines Backward</key>
    <string>moveToEndOfText:, moveToBeginningOfText:, deleteToBeginningOfLine:, deleteBackward:</string>
</dict>

Join Lines joins the lines forward, the other one backward.

After the file is saved, Xcode needs to be restarted to load the new setup.
In Xcode settings, the keyboard shorts can be configured:
Screenshot 2021-02-03 at 11 30 46

Helper method to execute an action on viewDidLoad of a UIViewController

How to execute an action on viewDidLoad of an UIViewController

private var onViewLoadContext: UInt8 = 0

public protocol UIViewControllerOnViewLoadProtocol { }

extension UIViewController: UIViewControllerOnViewLoadProtocol { }

private class OnViewControllerViewLoadObserver<T>: NSObject {
    private var handlers: [(T) -> Void]
    var token: NSKeyValueObservation?

    init (_ handler: @escaping (T) -> Void) {
        self.handlers = [handler]
    }

    func invoke(_ value: T) {
        self.handlers.forEach { handler in
            handler(value)
        }
    }

    func add(handler: @escaping (T) -> Void) {
        self.handlers.append(handler)
    }
}

public extension UIViewControllerOnViewLoadProtocol where Self: UIViewController {
    /// Add handler when view controller's view is set by the framework
    ///
    /// - Attention: Be aware of memory leak that can be caused if any reference holded by `self` is used
    /// inside the handler callback. For that purpose use the instance passed in the handler parameter.
    ///
    /// - Parameters:
    ///     - handler: Callback that will be called when UIKit will set view controller's view.
    ///     Instance passed in the handler parameter is optional because is weakified.
    ///
    /// - Tag: addOnViewLoad
    func addOnViewLoad(handler: @escaping (Self?) -> Void) {
        let observer: OnViewControllerViewLoadObserver<Self?>
        if let existingObserver = objc_getAssociatedObject(self, &onViewLoadContext) as? OnViewControllerViewLoadObserver<Self?> {
            observer = existingObserver
            observer.add(handler: handler)
        } else {
            observer = OnViewControllerViewLoadObserver(handler)
            objc_setAssociatedObject(
                self,
                &onViewLoadContext,
                observer,
                objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
            )

            observer.token = self.observe(\.view) { [weak observer, weak self] (_, _) in
                observer?.invoke(self)
            }
        }
    }

    /// Remove a handler added by [addOnViewLoad](x-source-tag://addOnViewLoad)
    func removeOnViewLoad() {
        objc_setAssociatedObject(
            self,
            &onViewLoadContext,
            nil,
            objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )
    }
}

ESPHome custom component vs component (contribution) development

Difference of development between custom component and full-fledged component for ESPHome

There are two approaches that can be used to develop a custom component for ESPHome:

  1. A simplified version described in the ESPHome documentation (Custom Sensor Compoent and Generic Custom Component)
  2. Native - "full-fledged" component that you can find e.g. ESPHome Build-in Components and its creation is described in the Contribution Guide

For better communication to distinguish components, I will call the former Simple and the latter Native.

The main difference between the two is that Native implementation provides better reusability, as you can use external_components feature to point to the local or remote (github) location in your .yaml device configuration to simply use some additional components except the build-in into ESPHome.
Additionally Native component has more advantages over the component configuration validation thanks to Python and configuration schemes.

The Simple component development is quite well described in the ESPHome documentation. In a nutshell, it just requires creating the .h + optionally .cpp file with the C++ (Arduino) component implementation and then the yaml file that registers the component so it can be used as any other ESPHome component. All that is clearly explained in the above documentation references.

In the case of the Native component development, this gets more tricky. The contribution guide sheds a bit of light on how to start but without many details. The most tricky part is how to properly write the __init__ and/orsensor.py which defines the component, validation, and C++ code generation (more details in the contribution guide). Most of the time it requires checking other component's implementations and based on that conclude what is actually needed. It's not that hard but requires a bit of Python knowledge. The C++ part is not that much different than the Simple component implementation. If you already have Simple implementation it can be copy/pasted, add namespace esphome, another inner for the component, and you are good to go.

If you would like to understand better the difference between the Simple and Native component implementation take a look at my repository. I implemented the same sensor using both approaches. You can find the Simple component implementation in custom_components/mq9 and Native in components/mq. Also, the example of how to use it is defined in mq9_test.yaml

The Native component is loaded thanks to the external_components definition in config_base.yaml.

To switch between the Native and Simple component usage uncomment the line that defines mq9_base in the mq9_test.yaml.

Observe deinitialization

An extension to allow observation when an object is deallocated.

// MARK: Deinit observation
extension NSObject {
    func onDeinit(execute work: @escaping () -> Void) {
        let deinitCallback = Self.deinitCallback(forObject: self)
        deinitCallback.callbacks.append(work)
    }

    // MARK: Helpers
    private static let key = UnsafeMutablePointer<UInt8>.allocate(capacity: 1)

    static private func deinitCallback(forObject object: NSObject) -> DeinitCallback {
        if let deinitCallback = objc_getAssociatedObject(object, key) as? DeinitCallback {
            return deinitCallback
        } else {
            let deinitCallback = DeinitCallback()
            objc_setAssociatedObject(object, key, deinitCallback, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return deinitCallback
        }
    }

    // MARK: Types
    @objc fileprivate class DeinitCallback: NSObject {
        var callbacks: [() -> Void] = []

        deinit {
            callbacks.forEach({ $0() })
        }
    }
}

Non Safe Area and Navigation Bar SwiftUI Hosting View Controller

How to hide navigation bar and fix the safe area issue while using UIHostingViewController since SwiftUI iOS 13.0

I created a UIHostingViewController that works for me since iOS 13.0, and solves the NavigationBar visibility issues and safe area in SwiftUI.

public final class NonSafeAreaHostingController<Content: View>: UIHostingController<Content> {
    public var navigationBarHidden = true
    public var statusBarHidden = true
    public var isEmbedded = false

    override public func viewDidLoad() {
        super.viewDidLoad()
        fixSafeAreaInsets()
    }

    override public func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(navigationBarHidden, animated: false)
    }

    override public var prefersStatusBarHidden: Bool {
        return statusBarHidden
    }

    override public var navigationController: UINavigationController? {
        isEmbedded ? nil : super.navigationController
    }

    private func fixSafeAreaInsets() {
        guard let _class = view?.classForCoder else {
            return
        }

        let safeAreaInsets: @convention(block) (AnyObject)
            -> UIEdgeInsets = { (_: AnyObject!) -> UIEdgeInsets in
                return .zero
            }

        guard let method = class_getInstanceMethod(
            _class.self,
            #selector(getter: UIView.safeAreaInsets)
        )
        else {
            return
        }

        class_replaceMethod(
            _class,
            #selector(getter: UIView.safeAreaInsets),
            imp_implementationWithBlock(safeAreaInsets),
            method_getTypeEncoding(method)
        )

        let safeAreaLayoutGuide: @convention(block) (AnyObject)
            -> UILayoutGuide? = { (_: AnyObject!) -> UILayoutGuide? in
                return nil
            }

        guard let method2 = class_getInstanceMethod(
            _class.self,
            #selector(getter: UIView.safeAreaLayoutGuide)
        )
        else {
            return
        }

        class_replaceMethod(
            _class,
            #selector(getter: UIView.safeAreaLayoutGuide),
            imp_implementationWithBlock(safeAreaLayoutGuide),
            method_getTypeEncoding(method2)
        )
    }
}

My Xcode themes

Xcode theme (light & dark)

Installation

Manual

Download and unzip
Plum_Xcode_Themes.zip
Copy *.xccolortheme files to ~/Library/Developer/Xcode/UserData/FontAndColorThemes/ (create the folder if doesn't exits yet)

Bash script

mkdir ./Plum_Xcode_Themes
curl --show-error --location https://github.com/nonameplum/blog/files/5965669/Plum_Xcode_Themes.zip | tar -xf - -C ./Plum_Xcode_Themes
cd ./Plum_Xcode_Themes
mkdir ~/Library/Developer/Xcode/UserData/FontAndColorThemes/
cp ./*.xccolortheme ~/Library/Developer/Xcode/UserData/FontAndColorThemes/

Light

Screenshot 2021-02-11 at 14 44 27

Dark

Screenshot 2021-02-11 at 14 45 04

Superview observer

How to observe the moment when the view is added to the super view.

Sometimes it is useful to know when the view has added a subview to another view. For example, it is required before we can activate the autolayout constraints.

This can be achieved by observing the moment when the view is moved to the window, as in the case of the UIView it happens at the time when is added as a subview.

To do that we can create UIView subclass that will override method didMoveToWindow().

public final class SuperviewObserver: UIView {
    private let observer: (_ this: UIView, _ superview: UIView) -> Void

    public init(observer: @escaping (_ this: UIView, _ superview: UIView) -> Void) {
        self.observer = observer
        super.init(frame: .zero)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func didMoveToWindow() {
        super.didMoveToWindow()
        guard let superview = superview?.superview else { return }

        observer(self, superview)
        removeFromSuperview()
    }
}

We can make this custom subclass more convenient to use, by creating an extension method:

public protocol UIViewProtocol {}
extension UIView: UIViewProtocol {}

public extension UIViewProtocol where Self: UIView {
    func onDidMoveToSubview(closure: @escaping (_ view: Self, _ superview: UIView) -> Void) {
        addSubview(
            SuperviewObserver { [unowned self] this, superview in
                closure(self, superview)
            }
        )
    }
}

UIViewProtocol protocol is added, to be able to get the exact type of the UIView.

let superview = UIView()
let view = CustomSubclass()
view.onDidMoveToSubview { this, superview in 
    NSLayoutConstraint.activate([
        this.leadingAnchor.constraint(equalTo: superview.leadingAnchor) // this is type of CustomSubclass
        ...
    ])
}

superview.addSubview(view)

Measure work with result using the newest Swift 5.7 api

How to measure async work duration and also receive the result

extension Clock {
    func measure<Result>(_ work: () async throws -> Result) async rethrows -> (duration: Self.Instant.Duration, result: Result) {
        var result: Result!
        let duration = try await self.measure {
            result = try await work()
        }

        return (duration, result)
    }
}

Thread safety dispatch_once Swifty alternative

Dispatch once in Swift using a handy trick

Switch allows to declare a type inside a function body.
This ability could be handy in some situations. I found it useful to run the body of the function only once.
For example we use to run only once the body of the function updateConstraints().
In addition to that, if we use static variable in the inner struct Swift gives us thread safety for free.
I have created a simple wrapper, that allows to run an action and be sure that it will be run only once, even in the concurrect scenario.

struct Once {
    func run(action: () -> Void) {
        struct RunCheck {
            static var didRun = false
        }

        guard !RunCheck.didRun else { return }

        RunCheck.didRun = true
        action()
    }
}

Example of use:

let once = Once()

var indexes: [Int] = []
DispatchQueue.concurrentPerform(iterations: 100, execute: { index in
    once.run {
        indexes.append(index)
    }
})

print(indexes) // It will have only one element

Unit Test Template for Xcode Playground

How to add custom template useful for the Unit Test directly in the Xcode Playgroud

Custom template that allows to write unit tests directly in the Xcode Playgroud with nice error/success messages:
Screenshot 2021-09-05 at 14 11 30
Screenshot 2021-09-05 at 14 14 28

Installation

Manual

Download and unzip
Unit Test.xctemplate.zip
Copy Unit Test.xctemplate directory to ~/Library/Developer/Xcode/Templates/File Templates/Playground/ (create the folder if doesn't exits yet)

Bash script

mkdir ./Unit_Test.xctemplate
curl --show-error --location https://github.com/nonameplum/blog/files/7111526/Unit.Test.xctemplate.zip | tar -xf - -C ./Unit_Test.xctemplate
cd ./Unit_Test.xctemplate
mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/Playground
cp -R "./Unit Test.xctemplate" ~/Library/Developer/Xcode/Templates/File\ Templates/Playground

Once you restart Xcode you should be able to see the Unit Test template for a new playgroud:
Screenshot 2021-09-05 at 14 05 01

Reverse engineered `_printHierarchy`

How to reverse engineer _printHierarchy

I wanted to traverse the view controllers hierarchy the same way as the private method that is available on UIKit -UIViewController._printHierarchy.

You can run it from lldb:

exp -l objc -o -- [UIApplication.sharedApplication.keyWindow.rootViewController _printHierarchy]

or using a class method

exp -l objc -o -- [UIViewController _printHierarchy]

and it should return something like:

<UITabBarController 0x12c01d200>, state: appeared, view: <UILayoutContainerView 0x129e07b40>
   | <UINavigationController 0x12a808200>, state: appeared, view: <UILayoutContainerView 0x129b10880>
   |    | <App.ViewController 0x129b04620>, state: disappeared, view: (view not loaded)
   |    | <App.ViewController 0x129b049c0>, state: disappeared, view: <UIView 0x129b27620> not in the window
   |    | <App.ViewController 0x129f06550>, state: disappeared, view: <UIView 0x129f0b7c0> not in the window
   |    | <App.ViewController 0x129b24540>, state: appeared, view: <UIView 0x129b2b750>
   | <UINavigationController 0x12b80a000>, state: disappeared, view: <UILayoutContainerView 0x12d0052c0> not in the window
   |    | <App.ViewController 0x129c07220>, state: disappeared, view: (view not loaded)
   |    | <App.ViewController 0x129c075c0>, state: disappeared, view: <UIView 0x129e0c730> not in the window
   |    | <App.ViewController 0x12d204900>, state: disappeared, view: <UIView 0x129e11550> not in the window
   |    | <App.ViewController 0x12d204ca0>, state: disappeared, view: <UIView 0x129b16200> not in the window
   + <App.ViewController 0x12d2065e0>, state: appeared, view: <UIView 0x129b26a90>, presented with: <_UIPageSheetPresentationController 0x12d205aa0>
   |    + <App.ViewController 0x129b4b130>, state: appeared, view: <UIView 0x129b4b4d0>, presented with: <_UIPageSheetPresentationController 0x129b4b7f0>
   |    |    + <App.ViewController 0x129e2ada0>, state: appeared, view: <UIView 0x129e25950>, presented with: <_UIPageSheetPresentationController 0x129e27ec0>

I have found a few existing solutions that for example use the UIResponder chain to traverse the hierarchy like this one.
But the problem with UIResponder.next is that it won't traverse all of the controllers if there is a branch of more than one UIViewController stack like you might have with e.g. UITabViewController and modally presented view controllers. UIResponder.next traverses bottom-up the view hierarchy but only from the specific leaf.

I thought, that why not reverse engineer the original _printHierarchy method, to check all of the quirks of its internal implementation and understand how Apple engineers traverse the view controllers hierarchy top-down.
It turned out to be quite straightforward after using Hopper Disassembler app.
The _printHierachy core method is actually implemented in _appendDescription. On UIKit 15.0 with disassembled UIKitCore using x86_64 which is a bit more readable than ARM64.

function _appendDescription {
    stack[-200] = rcx;
    r14 = rdx;
    r12 = [rdi retain];
    rbx = [rsi retain];
    if ([rbx length] != 0x0) {
            [rbx appendString:@"\n"];
    }
    stack[-208] = rbx;
    rax = [r12 _parentViewController];
    rax = [rax retain];
    stack[-188] = r14 & 0xff;
    stack[-216] = r12;
    if (rax != 0x0) {
            [rax release];
            r15 = stack[-208];
            if (stack[-200] != 0x0) {
                    r12 = sign_extend_64(stack[-200]);
                    rbx = 0x1;
                    do {
                            [r15 appendString:@"   | "];
                            rbx = rbx + 0x1;
                    } while (rbx <= r12);
            }
            r13 = stack[-216];
            rax = -[UIViewController _descriptionForPrintingHierarchyIncludingInsets:](r13, stack[-188]);
            rax = [rax retain];
            rbx = rax;
            [r15 appendString:rax];
    }
    else {
            if ([r12 _isRootViewController] != 0x0) {
                    r15 = stack[-208];
                    if (stack[-200] != 0x0) {
                            r12 = sign_extend_64(stack[-200]);
                            rbx = 0x1;
                            do {
                                    [r15 appendString:@"   | "];
                                    rbx = rbx + 0x1;
                            } while (rbx <= r12);
                    }
                    r13 = stack[-216];
                    rax = -[UIViewController _descriptionForPrintingHierarchyIncludingInsets:](r13, stack[-188]);
                    rax = [rax retain];
                    rbx = rax;
                    [r15 appendString:rax];
            }
            else {
                    r12 = stack[-208];
                    if (stack[-200] >= 0x2) {
                            rbx = sign_extend_64(stack[-200]) - 0x1;
                            do {
                                    [r12 appendString:@"   | "];
                                    rbx = rbx - 0x1;
                            } while (rbx != 0x0);
                    }
                    [r12 appendString:@"   + "];
                    r15 = [-[UIViewController _descriptionForPrintingHierarchyIncludingInsets:](stack[-216], stack[-188]) retain];
                    rax = [stack[-216] presentingViewController];
                    rax = [rax retain];
                    stack[-248] = rax;
                    rax = [rax _presentationController];
                    rax = [rax retain];
                    stack[-240] = rax;
                    rax = [rax _descriptionForPrintingViewControllerHierarchy];
                    rax = [rax retain];
                    stack[-224] = rax;
                    rax = [NSMutableString stringWithFormat:@"%@, presented with: %@", r15, rax];
                    rax = [rax retain];
                    r13 = stack[-216];
                    [r12 appendString:rax];
                    rbx = r15;
                    [rax release];
                    [stack[-224] release];
                    [stack[-240] release];
                    [stack[-248] release];
            }
    }
    [rbx release];
    *(int128_t *)(&stack[-312] + 0x30) = intrinsic_movaps(*(int128_t *)(&stack[-312] + 0x30), 0x0);
    *(int128_t *)(&stack[-312] + 0x20) = intrinsic_movaps(*(int128_t *)(&stack[-312] + 0x20), 0x0);
    *(int128_t *)(&stack[-312] + 0x10) = intrinsic_movaps(*(int128_t *)(&stack[-312] + 0x10), 0x0);
    *(int128_t *)&stack[-312] = intrinsic_movaps(*(int128_t *)&stack[-312], 0x0);
    rax = [r13 childViewControllers];
    rax = [rax retain];
    rbx = rax;
    rcx = &stack[-184];
    rdx = &stack[-312];
    rax = [rax countByEnumeratingWithState:rdx objects:rcx count:0x10];
    if (rax != 0x0) {
            r13 = rax;
            r12 = **(&stack[-312] + 0x10);
            r15 = stack[-200] + 0x1;
            do {
                    r14 = 0x0;
                    do {
                            if (*stack[-296] != r12) {
                                    objc_enumerationMutation(rbx);
                            }
                            _appendDescription(*(stack[-304] + r14 * 0x8), stack[-208], stack[-188], r15, 0x10);
                            r14 = r14 + 0x1;
                    } while (r13 != r14);
                    rdx = &stack[-312];
                    rcx = &stack[-184];
                    rax = [rbx countByEnumeratingWithState:rdx objects:rcx count:0x10];
                    r13 = rax;
            } while (rax != 0x0);
    }
    [rbx release];
    r14 = stack[-216];
    rsi = @selector(childModalViewController);
    rax = (*_objc_msgSend)(r14, rsi);
    rax = [rax retain];
    [rax release];
    rbx = stack[-208];
    if (rax != 0x0) {
            rax = [r14 childModalViewController];
            rax = [rax retain];
            rcx = stack[-200] + 0x1;
            rsi = rbx;
            rdx = stack[-188];
            _appendDescription(rax, rsi, rdx, rcx);
            [rax release];
    }
    stack[-56] = **___stack_chk_guard;
    [rbx release];
    [r14 release];
    rax = *___stack_chk_guard;
    rax = *rax;
    if (rax != stack[-56]) {
            rax = __stack_chk_fail();
    }
    return rax;
}

There are few if statements that split the code, but they are mostly used to properly append to the print indentation, like " | " and " + ". Overall if you forget about the lines generated by the assembly code (e.g. registry assignments) and ARC (e.g. [rax retain], [r14 release]) the code is quite short and simple. Inside if statements blocks you can find the while/repeat loops that add the indentation required depending on the view hierarchy tree.

I transferred the code into Swift and this is what I got:

extension UIViewController {
    public func printHierarchy() {
        var prints: [String] = []
        self.appendDescription(output: &prints, deep: 0)

        let printString = prints.reduce("") { partialResult, elem in
            return partialResult.appending(elem)
        }

        print(printString)
    }

    private func appendDescription(output: inout [String], deep: Int) {
        if !output.isEmpty {
            output.append("\n")
        }

        if self.parent != nil {
            var i = 0
            while i < deep {
                output.append("   | ")
                i += 1
            }
            output.append(self.description)
        } else {
            if self.isRootViewController {
                var i = 1
                while i < deep {
                    output.append("   | ")
                    output.append(self.description)
                    i += 1
                }
                output.append(self.description)
            } else {
                if deep >= 2 {
                    var i = deep - 1
                    repeat {
                        output.append("   | ")
                        i -= 1
                    } while i != 0
                }
                output.append("   + ")
                output.append(self.description)
                output.append(", presented with: \(self.presentationController?.description ?? "N/A")")
            }
        }

        if !self.children.isEmpty {
            var i = 0
            while i < self.children.count {
                self.children[i].appendDescription(output: &output, deep: deep + 1)
                i += 1
            }
        }

        if let childModalVC = self.childModalViewController {
            childModalVC.appendDescription(output: &output, deep: deep + 1)
        }
    }
}

extension UIViewController {
    var isRootViewController: Bool {
        UIApplication.shared.windows.compactMap(\.rootViewController).contains(self)
    }

    var childModalViewController: UIViewController? {
        if self.presentedViewController?.presentingViewController == self {
            return self.presentedViewController
        } else {
            return nil
        }
    }
}

I intentionally left the while/repeat loops to make the comparison with disassembled method easier. I had to reimplement the isRootViewController and childModalViewController that is used in the original implementation and it is not avaiable in the public methods of UIKit API. appendDescription goes recursively trought child and child modal view controllers.
What I learned from this, is that Apple engenieers smartly do not check for the specific UIViewController subclasses to verify if there are sub view controllers like you can do with UINavigationController.viewControllers. They just check if there are child/modal view controllers, and recursively visit all of them.
The ouput of my reimplemented Swift printHierarchy looks the same beside slight differences of the UIViewControllers descriptions, as again there is custom implementation used internally like _descriptionForPrintingHierarchyIncludingInsets.

Reimplemented

<UITabBarController: 0x12c01d200>
   | <UINavigationController: 0x12a808200>
   |    | <App.ViewController: 0x129b04620>
   |    | <App.ViewController: 0x129b049c0>
   |    | <App.ViewController: 0x129f06550>
   |    | <App.ViewController: 0x129b24540>
   | <UINavigationController: 0x12b80a000>
   |    | <App.ViewController: 0x129c07220>
   |    | <App.ViewController: 0x129c075c0>
   |    | <App.ViewController: 0x12d204900>
   |    | <App.ViewController: 0x12d204ca0>
   + <App.ViewController: 0x12d2065e0>, presented with: <_UIPageSheetPresentationController: 0x12d205aa0>
   |    + <App.ViewController: 0x129b4b130>, presented with: <_UIPageSheetPresentationController: 0x129b4b7f0>
   |    |    + <App.ViewController: 0x129e2ada0>, presented with: <_UIPageSheetPresentationController: 0x129e27ec0>

Original

<UITabBarController 0x12c01d200>, state: appeared, view: <UILayoutContainerView 0x129e07b40>
  | <UINavigationController 0x12a808200>, state: appeared, view: <UILayoutContainerView 0x129b10880>
  |    | <App.ViewController 0x129b04620>, state: disappeared, view: (view not loaded)
  |    | <App.ViewController 0x129b049c0>, state: disappeared, view: <UIView 0x129b27620> not in the window
  |    | <App.ViewController 0x129f06550>, state: disappeared, view: <UIView 0x129f0b7c0> not in the window
  |    | <App.ViewController 0x129b24540>, state: appeared, view: <UIView 0x129b2b750>
  | <UINavigationController 0x12b80a000>, state: disappeared, view: <UILayoutContainerView 0x12d0052c0> not in the window
  |    | <App.ViewController 0x129c07220>, state: disappeared, view: (view not loaded)
  |    | <App.ViewController 0x129c075c0>, state: disappeared, view: <UIView 0x129e0c730> not in the window
  |    | <App.ViewController 0x12d204900>, state: disappeared, view: <UIView 0x129e11550> not in the window
  |    | <App.ViewController 0x12d204ca0>, state: disappeared, view: <UIView 0x129b16200> not in the window
  + <App.ViewController 0x12d2065e0>, state: appeared, view: <UIView 0x129b26a90>, presented with: <_UIPageSheetPresentationController 0x12d205aa0>
  |    + <App.ViewController 0x129b4b130>, state: appeared, view: <UIView 0x129b4b4d0>, presented with: <_UIPageSheetPresentationController 0x129b4b7f0>
  |    |    + <App.ViewController 0x129e2ada0>, state: appeared, view: <UIView 0x129e25950>, presented with: <_UIPageSheetPresentationController 0x129e27ec0>

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.