Skip to content

Releases: teufelaudio/FoundationExtensions

v0.3.1

09 Mar 16:09
0aa9050
Compare
Choose a tag to compare
  • Add TypePathConvertible and default implementation to create a string out of the type hierarchy.

v0.3.0

01 Mar 16:09
079dcc3
Compare
Choose a tag to compare
  • Add MutableParameter.
  • Add '.eraseFailureToError()' 'Publisher' and 'Promise' types.
  • Update file headers.

v0.2.0

30 Jan 09:37
c837d87
Compare
Choose a tag to compare
  • Add mutate free function.
  • Add .nilOutIfEmpty for the String.

Added CombineLatest7

17 Jan 17:03
6bf3ae7
Compare
Choose a tag to compare
Added combine latest 7 struct (#41)

Co-authored-by: Giulia Ariu <>

v0.1.19

24 Nov 13:33
def4b8d
Compare
Choose a tag to compare

Add L10n

Gives type-safe localization in 3 easy steps ✨.

Step 1: Define your strings in an extension of L10n (i.e. in App/Sources/L10n/L10n.swift).

extension L10n {
    public enum HomeScreen: Localizable {
        // sourcery: comment=Navigation Bar Title on Home screen
        case navigationBarTitle

        // sourcery: comment=Some value with parameters
        case value(/* sourcery: format=%d */ count: Int, another: String, /* sourcery: format=%.2f */ exacly: Float)

        // sourcery: comment=Title for submit button on Home screen
        case submitButton
    }
}

Step 2: Use sourcery + genstrings + MergeL10n to generate Localizable.strings.
See .sourcery-l10n-example.yml and adjust the paths to your project. The sourcery template
can be found in Tools/Sourcery/Templates/Sources/Localizable.stencil.

# Makefile excerpt
App/Sources/Generated/Localizable.generated.swift: App/Sources/L10n/L10n.swift
    sourcery --config App/Sources/.sourcery-l10n.yml
    genstrings -o App/Sources/Resources/zz.lproj -s LS App/Sources/Generated/Localizable.generated.swift
    Tools/MergeL10n pseudo-to-languages --base-paths="App/Sources/Resources"

Step 3: Refer to the strings using the .localizedString method.

public struct Example: View {
    public var body: some View {
        Text(L10n.HomeScreen.navigationBarTitle.localizedString)
    }
}

New transformation function in CGFloat extension

04 Oct 22:53
9eacde8
Compare
Choose a tag to compare
Interpolated CGFloat (#37)

Co-authored-by: Giulia Ariu <>

0.1.17

06 Sep 14:13
79ebeb6
Compare
Choose a tag to compare

Making FoundationExtensions the static default.

Renamed static / dynamic, make sure that packages without any suffix are static. This unifies a inconsistency in a lot of frameworks used in our products. Also, static linking is is an attempt to fix this issue while deploying to TestFlight / AppStore:

ITMS-90334: Invalid Code Signature Identifier. The identifier "FoundationExtensions-5555494443d5626ab868338a93cce6b274e34595"
in your code signature for "FoundationExtensions" must match its Bundle Identifier "FoundationExtensions"

This fixes SwiftUI previews in Swift Packages as well.

FoundationExtensions is the default now, the dynamic product is FoundationExtensionsDynamic.

0.1.16

06 Jul 13:25
503d411
Compare
Choose a tag to compare

What's Changed

  • Adds Data(hex:) to allow creating Data from hex-Strings (without prefix) by @melle in #35

Improve Promise unsafe methods for clarity

22 Apr 10:02
34cba24
Compare
Choose a tag to compare
Improve Promise unsafe methods for clarity (#34)

* Improve Promise unsafe methods for clarity

* Fix indentation

Avoid Combine FlatMap

25 Jan 17:49
21010b4
Compare
Choose a tag to compare

Apparently, Combine FlatMap leaks memory, never cancelling the upstream when, from inside its block, you return a Fail.
Other operators like tryMap and flatMapLatest (map+switchToLatest) work as expected.
Let's check the following example:

cancellable = Timer
    .publish(every: 2, on: .main, in: .default)
    .autoconnect()
    .print("🤷‍♂️ point A")
    .scan(0, { result, _ in
        result + 1
    })
    .print("🤷‍♂️ point B")
    .setFailureType(to: Error.self)
    .flatMap { int -> AnyPublisher<Int, Error> in
        if int == 4 {
            return Fail<Int, Error>(error: BlaError()).eraseToAnyPublisher()
        }
        return Just<Int>(int).setFailureType(to: Error.self).eraseToAnyPublisher()
    }
    .print("🤷‍♂️ point C")
    .sink { (completion: Subscribers.Completion<Error>) in
        print("🤷‍♂️ completion: \(completion)")
    } receiveValue: { (value: Int) in
        print("🤷‍♂️", value)
    }

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(15)) {
    print("🤷‍♂️ Cancelling")
    cancellable = nil
}

The example above shows a Timer that starts feeding a flatMap, which on the 4th event sends a Fail. As the Fail reaches the main pipeline, we expect it to complete the whole pipeline and tell the Timer to stop (cancel). This is what happens with all the publishers upon failure, it propagates downstream with the failure but even before that it tells all upstreams to cancel.

But not flatMap. It propagates the error to the downstream but forgets to tell upstream to cancel, which causes the timer to keep rolling forever and ever. To make it worse, calling .cancel on the subscription also doesn't stop the timer. Even if everything goes out of scope now, the Timer will remain alive.

If it's not a timer, but something like a LongPolling, WebSocket or whatever side-effect upstream does, the requests will continue forever, wasting bandwidth, battery and memory, and eventually also causing bugs.

This is the output for the example above:

🤷‍♂️ point C: receive subscription: (FlatMap)
🤷‍♂️ point C: request unlimited
🤷‍♂️ point A: receive subscription: ((extension in Foundation):__C.NSTimer.TimerPublisher.Inner<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>.(unknown context at $7fff4edb7680).Inner<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>.(unknown context at $7fff4edb7a18).Inner<Combine.Publishers.Scan<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>, Swift.Int>.(unknown context at $7fff4edbdad0).Inner<Combine.Publishers.Print<Combine.Publishers.Scan<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>, Swift.Int>>.(unknown context at $7fff4edb7a18).Inner<Combine.Publishers.SetFailureType<Combine.Publishers.Print<Combine.Publishers.Scan<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>, Swift.Int>>, Swift.Error>.(unknown context at $7fff4edb8a80).Inner<Combine.Publishers.FlatMap<Combine.AnyPublisher<Swift.Int, Swift.Error>, Combine.Publishers.SetFailureType<Combine.Publishers.Print<Combine.Publishers.Scan<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>, Swift.Int>>, Swift.Error>>.(unknown context at $7fff4edc4580).Outer<Combine.Publishers.Print<Combine.Publishers.FlatMap<Combine.AnyPublisher<Swift.Int, Swift.Error>, Combine.Publishers.SetFailureType<Combine.Publishers.Print<Combine.Publishers.Scan<Combine.Publishers.Print<Combine.Publishers.Autoconnect<(extension in Foundation):__C.NSTimer.TimerPublisher>>, Swift.Int>>, Swift.Error>>>.(unknown context at $7fff4edb7a18).Inner<Combine.Subscribers.Sink<Swift.Int, Swift.Error>>>, Swift.Error>>>>>>)
🤷‍♂️ point B: receive subscription: (Print)
🤷‍♂️ point B: request unlimited
🤷‍♂️ point A: request unlimited
🤷‍♂️ point A: receive value: (2022-01-25 17:38:19 +0000)
🤷‍♂️ point B: receive value: (1)
🤷‍♂️ point C: receive value: (1)
🤷‍♂️ 1
🤷‍♂️ point A: receive value: (2022-01-25 17:38:21 +0000)
🤷‍♂️ point B: receive value: (2)
🤷‍♂️ point C: receive value: (2)
🤷‍♂️ 2
🤷‍♂️ point A: receive value: (2022-01-25 17:38:23 +0000)
🤷‍♂️ point B: receive value: (3)
🤷‍♂️ point C: receive value: (3)
🤷‍♂️ 3
🤷‍♂️ point A: receive value: (2022-01-25 17:38:25 +0000)
🤷‍♂️ point B: receive value: (4)
🤷‍♂️ point C: receive error: (BlaError())
🤷‍♂️ completion: failure(TeufelStreaming_iOS.BlaError())
🤷‍♂️ point A: receive value: (2022-01-25 17:38:27 +0000)
🤷‍♂️ point B: receive value: (5)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:29 +0000)
🤷‍♂️ point B: receive value: (6)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:31 +0000)
🤷‍♂️ point B: receive value: (7)
🤷‍♂️ Cancelling
🤷‍♂️ point A: receive value: (2022-01-25 17:38:33 +0000)
🤷‍♂️ point B: receive value: (8)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:35 +0000)
🤷‍♂️ point B: receive value: (9)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:37 +0000)
🤷‍♂️ point B: receive value: (10)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:39 +0000)
🤷‍♂️ point B: receive value: (11)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:41 +0000)
🤷‍♂️ point B: receive value: (12)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:43 +0000)
🤷‍♂️ point B: receive value: (13)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:45 +0000)
🤷‍♂️ point B: receive value: (14)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:47 +0000)
🤷‍♂️ point B: receive value: (15)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:49 +0000)
🤷‍♂️ point B: receive value: (16)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:51 +0000)
🤷‍♂️ point B: receive value: (17)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:53 +0000)
🤷‍♂️ point B: receive value: (18)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:55 +0000)
🤷‍♂️ point B: receive value: (19)
🤷‍♂️ point A: receive value: (2022-01-25 17:38:57 +0000)
🤷‍♂️ point B: receive value: (20)

Points A and B keep sending values even after cancellation. And FlatMap, in a faulty state, ignores both, the cancelation from the downstream, and the events from the upstream.

Moving to map+switchToLatest fixes the problem.