Skip to content

Commit

Permalink
Implemented 'Refreshable' property wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
Alkenso committed Apr 12, 2022
1 parent 624b263 commit 2b8a253
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 7 deletions.
102 changes: 102 additions & 0 deletions Sources/SwiftConvenience/Types & PropertyWrappers/Refreshable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// MIT License
//
// Copyright (c) 2022 Alkenso (Vladimir Vashurkin)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation

@propertyWrapper
public struct Refreshable<Value> {
private var innerValue: Value? { didSet { expiration.onUpdate(innerValue!) } }
private let expiration: Expiration
private let source: Source

public init(wrappedValue: Value? = nil, expire: Expiration, source: Source) {
self.innerValue = wrappedValue
self.expiration = expire
self.source = source

if let innerValue = innerValue {
self.expiration.onUpdate(innerValue)
}
}

public var wrappedValue: Value {
mutating get {
if let innerValue = innerValue {
if expiration.checkExpired(innerValue) {
self.innerValue = source.newValue(innerValue)
}
} else {
innerValue = source.newValue(nil)
}
return innerValue!
}
set {
innerValue = newValue
}
}
}

extension Refreshable {
public init(wrappedValue: Value? = nil, expire: Expiration) where Value: ExpressibleByNilLiteral {
self.init(wrappedValue: wrappedValue, expire: expire, source: .defaultNil())
}
}

extension Refreshable {
public struct Expiration {
public var checkExpired: (Value) -> Bool
public var onUpdate: (Value) -> Void

public init(checkExpired: @escaping (Value) -> Bool, onUpdate: @escaping (Value) -> Void) {
self.checkExpired = checkExpired
self.onUpdate = onUpdate
}
}

public struct Source {
public var newValue: (_ old: Value?) -> Value

public init(newValue: @escaping (Value?) -> Value) {
self.newValue = newValue
}
}
}

extension Refreshable.Expiration {
public static func ttl(_ duration: TimeInterval) -> Self {
var expirationDate = Date()
return .init(
checkExpired: { _ in expirationDate < Date() },
onUpdate: { _ in expirationDate = Date().addingTimeInterval(duration) }
)
}
}

extension Refreshable.Source {
public static func defaultValue(_ defaultValue: Value) -> Self {
.init(newValue: { _ in defaultValue })
}

public static func defaultNil() -> Self where Value: ExpressibleByNilLiteral {
.init(newValue: { _ in nil })
}
}
7 changes: 0 additions & 7 deletions Tests/SwiftConvenienceTests/ObjCTests.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// File.swift
//
//
// Created by Alkenso (Vladimir Vashurkin) on 20.11.2021.
//

import SwiftConvenience
import XCTest

Expand Down
42 changes: 42 additions & 0 deletions Tests/SwiftConvenienceTests/RefreshableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import SwiftConvenience
import XCTest

class RefreshableTests: XCTestCase {
func test_basic() {
var expired = false
var newValue = 1
var value = Refreshable(
wrappedValue: 10,
expire: .init(checkExpired: { _ in expired }, onUpdate: { _ in expired = false }),
source: .init(newValue: { _ in newValue })
)

// Refreshable holds value it initialized with
XCTAssertEqual(value.wrappedValue, 10)

// Mark Refreshable as expired. Value should be updated, expired state reset
expired = true
XCTAssertEqual(value.wrappedValue, 1)
XCTAssertEqual(expired, false)

// New value will be picked up only when previous is expired
newValue = 20
XCTAssertEqual(value.wrappedValue, 1)
expired = true
XCTAssertEqual(value.wrappedValue, 20)
}

func test_ttl() {
var value = Refreshable(
wrappedValue: 10,
expire: .ttl(0.05),
source: .defaultValue(1)
)

XCTAssertEqual(value.wrappedValue, 10)

// Value is expired after 0.1 and reset to default
Thread.sleep(forTimeInterval: 0.1)
XCTAssertEqual(value.wrappedValue, 1)
}
}

0 comments on commit 2b8a253

Please sign in to comment.