Skip to content

Commit

Permalink
154
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Jul 28, 2021
1 parent ea63fe0 commit 4cdd204
Show file tree
Hide file tree
Showing 13 changed files with 1,149 additions and 0 deletions.
505 changes: 505 additions & 0 deletions 0154-refreshable-pt2/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC89C41224460F95006900B9"
BuildableName = "SwiftUICaseStudies.app"
BlueprintName = "SwiftUICaseStudies"
ReferencedContainer = "container:CaseStudies.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC89C42824460F96006900B9"
BuildableName = "SwiftUICaseStudiesTests.xctest"
BlueprintName = "SwiftUICaseStudiesTests"
ReferencedContainer = "container:CaseStudies.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC89C41224460F95006900B9"
BuildableName = "SwiftUICaseStudies.app"
BlueprintName = "SwiftUICaseStudies"
ReferencedContainer = "container:CaseStudies.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "DC89C41224460F95006900B9"
BuildableName = "SwiftUICaseStudies.app"
BlueprintName = "SwiftUICaseStudies"
ReferencedContainer = "container:CaseStudies.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "AppIcon.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Combine
import ComposableArchitecture
import SwiftUI

struct PullToRefreshState: Equatable {
var count = 0
var fact: String?
var isLoading = false
}

enum PullToRefreshAction: Equatable {
case cancelButtonTapped
case factResponse(Result<String, FactClient.Error>)
case decrementButtonTapped
case incrementButtonTapped
case refresh
}

struct PullToRefreshEnvironment {
var fact: FactClient
var mainQueue: AnySchedulerOf<DispatchQueue>
}

let pullToRefreshReducer = Reducer<
PullToRefreshState,
PullToRefreshAction,
PullToRefreshEnvironment
> { state, action, environment in

struct CancelId: Hashable {}

switch action {
case .cancelButtonTapped:
state.isLoading = false
return .cancel(id: CancelId())

case let .factResponse(.success(fact)):
state.isLoading = false
state.fact = fact
return .none

case .factResponse(.failure):
state.isLoading = false
// TODO: do some error handling
return .none

case .decrementButtonTapped:
state.count -= 1
return .none

case .incrementButtonTapped:
state.count += 1
return .none

case .refresh:
state.fact = nil
state.isLoading = true
return environment.fact.fetch(state.count)
// .receive(on: environment.mainQueue)
.delay(for: 2, scheduler: environment.mainQueue.animation())
.catchToEffect()
.map(PullToRefreshAction.factResponse)
.cancellable(id: CancelId())
}
}

struct PullToRefreshView: View {
let store: Store<PullToRefreshState, PullToRefreshAction>

var body: some View {
WithViewStore(self.store) { viewStore in
List {
HStack {
Button("-") { viewStore.send(.decrementButtonTapped) }
Text("\(viewStore.count)")
Button("+") { viewStore.send(.incrementButtonTapped) }
}
.buttonStyle(.plain)

if let fact = viewStore.fact {
Text(fact)
}
if viewStore.isLoading {
Button("Cancel") {
viewStore.send(.cancelButtonTapped, animation: .default)
}
}
}
.refreshable {
await viewStore.send(.refresh, while: \.isLoading)
}
}
}
}

extension ViewStore {
func send(
_ action: Action,
while predicate: @escaping (State) -> Bool
) async {
self.send(action)
await withUnsafeContinuation { (continuation: UnsafeContinuation<Void, Never>) in
var cancellable: Cancellable?
cancellable = self.publisher
.filter { !predicate($0) }
.prefix(1)
.sink { _ in
continuation.resume()
_ = cancellable
}
}
}
}

struct PullToRefresh_Previews: PreviewProvider {
static var previews: some View {
PullToRefreshView(
store: .init(
initialState: .init(),
reducer: pullToRefreshReducer,
environment: PullToRefreshEnvironment(
fact: .live,
mainQueue: .main
)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Combine
import ComposableArchitecture
import XCTestDynamicOverlay

struct FactClient {
var fetch: (Int) -> Effect<String, Error>

struct Error: Swift.Error, Equatable {}
}

extension FactClient {
// This is the "live" fact dependency that reaches into the outside world to fetch trivia.
// Typically this live implementation of the dependency would live in its own module so that the
// main feature doesn't need to compile it.
static let live = Self(
fetch: { number in
URLSession.shared.dataTaskPublisher(
for: URL(string: "http://numbersapi.com/\(number)/trivia")!
)
.map { data, _ in String(decoding: data, as: UTF8.self) }
.catch { _ in
// Sometimes numbersapi.com can be flakey, so if it ever fails we will just
// default to a mock response.
Just("\(number) is a good number Brent")
.delay(for: 1, scheduler: DispatchQueue.main)
}
.setFailureType(to: Error.self)
.eraseToEffect()
})
}

#if DEBUG
extension FactClient {
// This is the "failing" fact dependency that is useful to plug into tests that you want
// to prove do not need the dependency.
static let failing = Self(
fetch: { _ in
XCTFail("\(Self.self).fact is unimplemented.")
return .none
})
}
#endif
Loading

0 comments on commit 4cdd204

Please sign in to comment.