Created
July 1, 2020 21:04
-
-
Save pd95/a39c4bd69f3a569de99c7e0682716ac2 to your computer and use it in GitHub Desktop.
Illustrate Memory leak in Publisher.replaceError
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// Bug-Combine-Leak | |
// | |
import Combine | |
import SwiftUI | |
final class Store: ObservableObject { | |
struct User: Codable { | |
let login: String | |
let avatar_url: String | |
let name: String? | |
} | |
@Published var user: User? | |
@Published var userImage: UIImage = UIImage(systemName: "photo")! | |
var cancellables = Set<AnyCancellable>() | |
let baseURL = URL(string: "https://api.github.com/")! | |
func fetchUserInfo(_ user: String) { | |
let request = URLRequest(url: baseURL.appendingPathComponent("users/\(user)")) | |
URLSession.shared | |
.dataTaskPublisher(for: request) | |
.map { $0.data } | |
.decode(type: User?.self, decoder: JSONDecoder()) | |
.replaceError(with: nil) // FIXME | |
.receive(on: RunLoop.main) | |
.sink(receiveCompletion: { (result) in | |
print("fetchUserInfo result", result) | |
}) { (user: User?) in | |
self.user = user | |
} | |
.store(in: &cancellables) | |
} | |
func fetchAvatar() { | |
if let avatarUrl = URL(string: user?.avatar_url ?? "") { | |
URLSession.shared | |
.dataTaskPublisher(for: URLRequest(url: avatarUrl)) | |
.map { $0.data } | |
.tryMap { UIImage(data: $0)! } | |
.replaceError(with: UIImage(systemName: "photo")!) // FIXME | |
.receive(on: DispatchQueue.main) | |
.assign(to: \.userImage, on: self) | |
.store(in: &cancellables) | |
} | |
} | |
} | |
struct ContentView: View { | |
@ObservedObject var store: Store | |
@State private var username = "pd95" | |
@State private var loadAvatar = false | |
var body: some View { | |
VStack { | |
Text("Who is?") | |
.font(.largeTitle) | |
.bold() | |
TextField("Github username", text: $username, onCommit: fetchUser) | |
.textContentType(.username) | |
.font(.headline) | |
.padding() | |
.background(RoundedRectangle(cornerRadius: 8).stroke(Color.secondary)) | |
HStack { | |
Button(action: fetchUser) { | |
Text("Check") | |
.font(.headline) | |
.padding() | |
.background(RoundedRectangle(cornerRadius: 8).fill(Color.blue)) | |
.foregroundColor(Color.white) | |
} | |
.padding() | |
if store.user != nil { | |
Button(action: fetchAvatar) { | |
Text("Load Avatar") | |
.font(.headline) | |
.padding() | |
.background(RoundedRectangle(cornerRadius: 8).fill(Color.yellow)) | |
} | |
.padding() | |
} | |
} | |
Divider() | |
if store.user != nil { | |
Text(store.user?.login ?? "n/a") | |
Text(store.user?.name ?? "n/a") | |
if loadAvatar { | |
Image(uiImage: store.userImage) | |
.resizable() | |
.scaledToFit() | |
} | |
} | |
} | |
.padding() | |
} | |
func fetchUser() { | |
loadAvatar = false | |
self.store.fetchUserInfo(self.username) | |
} | |
func fetchAvatar() { | |
loadAvatar = true | |
self.store.fetchAvatar() | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView(store: Store()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This code illustrates a memory leak bug I've come across while using SwiftUI and Combine.
The example does the following:
When checking for memory leaks on each step, you will see that after "Check" there are 3 leaks and it's clear from the details that they are caused by
Store.fetchUserInfo
.And after "Load Avatar" I see 6 memory issues caused by
Store.fetchAvatar
.To fix those issues, we have to comment
.replaceError(with: ...)
(on line 29 and line 45) .For me, this tastes like a real bug in Combine framework.
Any comments are welcome :)