From f2bf132d6df251ea7d5224d0d86c2ca5b0c02320 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 15 May 2024 09:38:05 +0300 Subject: Integrate feeds from good old `tst` 2019 project --- JuickNext/FeedView.swift | 55 ++++++++++++++++++++++++++++++++++++++ JuickNext/Helpers/Image+Data.swift | 35 ++++++++++++++++++++++++ JuickNext/ImageFetcher.swift | 28 +++++++++++++++++++ JuickNext/LoadableImageView.swift | 29 ++++++++++++++++++++ JuickNext/LoadableState.swift | 25 +++++++++++++++++ JuickNext/MessageFetcher.swift | 39 +++++++++++++++++++++++++++ JuickNext/MessageView.swift | 43 +++++++++++++++++++++++++++++ JuickNext/Models.swift | 47 ++++++++++++++++++++++++++++++++ JuickNext/View/ContentView.swift | 13 ++++++--- JuickNext/View/Discussions.swift | 21 --------------- JuickNext/View/Today.swift | 24 ----------------- 11 files changed, 310 insertions(+), 49 deletions(-) create mode 100644 JuickNext/FeedView.swift create mode 100644 JuickNext/Helpers/Image+Data.swift create mode 100644 JuickNext/ImageFetcher.swift create mode 100644 JuickNext/LoadableImageView.swift create mode 100644 JuickNext/LoadableState.swift create mode 100644 JuickNext/MessageFetcher.swift create mode 100644 JuickNext/MessageView.swift create mode 100644 JuickNext/Models.swift delete mode 100644 JuickNext/View/Discussions.swift delete mode 100644 JuickNext/View/Today.swift (limited to 'JuickNext') diff --git a/JuickNext/FeedView.swift b/JuickNext/FeedView.swift new file mode 100644 index 0000000..399da1f --- /dev/null +++ b/JuickNext/FeedView.swift @@ -0,0 +1,55 @@ +// +// FeedView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI + +struct FeedView: View { + @ObservedObject var messageFetcher : MessageFetcher + + let title: String + + init(_ title: String, url: String) { + self.title = title + messageFetcher = MessageFetcher(url: url) + } + + private var stateContent: AnyView { + switch messageFetcher.state { + case .loading: + return AnyView( + ProgressView() + ) + case .fetched(let result): + switch result { + case .failure(let error): + return AnyView( + Text(error.localizedDescription) + ) + case .success(let root): + return AnyView( + List(root) { (message: Message) in + MessageView(message: message) + } + ) + } + } + } + + var body: some View { + NavigationView { + stateContent + .navigationBarTitle(Text(title), displayMode: .inline) + } + } +} + +struct FeedView_Previews: PreviewProvider { + static var previews: some View { + FeedView("Discover", url: "https://api.juick.com/messages") + } +} diff --git a/JuickNext/Helpers/Image+Data.swift b/JuickNext/Helpers/Image+Data.swift new file mode 100644 index 0000000..dee2693 --- /dev/null +++ b/JuickNext/Helpers/Image+Data.swift @@ -0,0 +1,35 @@ +// +// Image+Data.swift +// JuickNext +// +// Created by Vitaly Takmazov on 15.05.2024. +// Copyright © 2024 com.juick. All rights reserved. +// + +import Foundation +import SwiftUI +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif +extension Image { + /// Initializes a SwiftUI `Image` from data. + init?(data: Data) { +#if canImport(UIKit) + if let uiImage = UIImage(data: data) { + self.init(uiImage: uiImage) + } else { + return nil + } +#elseif canImport(AppKit) + if let nsImage = NSImage(data: data) { + self.init(nsImage: nsImage) + } else { + return nil + } +#else + return nil +#endif + } +} diff --git a/JuickNext/ImageFetcher.swift b/JuickNext/ImageFetcher.swift new file mode 100644 index 0000000..f76c0ba --- /dev/null +++ b/JuickNext/ImageFetcher.swift @@ -0,0 +1,28 @@ +// +// ImageFetcher.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation +import Combine + +class ImageFetcher: ObservableObject { + + @Published var data: Data = Data() + + init(url: String) { + guard let imageUrl = URL(string: url) else { + return + } + + URLSession.shared.dataTask(with: imageUrl) { (data, _, _) in + guard let data = data else { return } + DispatchQueue.main.async { [weak self] in + self?.data = data + } + }.resume() + } +} diff --git a/JuickNext/LoadableImageView.swift b/JuickNext/LoadableImageView.swift new file mode 100644 index 0000000..6a41dc2 --- /dev/null +++ b/JuickNext/LoadableImageView.swift @@ -0,0 +1,29 @@ +// +// LoadableImageView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI + +struct LoadableImageView: View { + @ObservedObject var imageFetcher: ImageFetcher + + init(with urlString: String) { + imageFetcher = ImageFetcher(url: urlString) + } + + var body: some View { + if let image = Image(data: imageFetcher.data) { + return AnyView( + image.resizable() + ) + } else { + return AnyView( + ProgressView() + ) + } + } +} diff --git a/JuickNext/LoadableState.swift b/JuickNext/LoadableState.swift new file mode 100644 index 0000000..a45edb2 --- /dev/null +++ b/JuickNext/LoadableState.swift @@ -0,0 +1,25 @@ +// +// LoadableState.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation + +enum LoadableState { + case loading + case fetched(Result) +} + +enum FetchError: Error { + case error(String) + + var localizedDescription: String { + switch self { + case .error(let message): + return message + } + } +} diff --git a/JuickNext/MessageFetcher.swift b/JuickNext/MessageFetcher.swift new file mode 100644 index 0000000..565cca0 --- /dev/null +++ b/JuickNext/MessageFetcher.swift @@ -0,0 +1,39 @@ +// +// MessageFetcher.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation +import Combine + +class MessageFetcher: ObservableObject { + + @Published var state: LoadableState = .loading + + init(url: String) { + guard let apiUrl = URL(string: url) else { + state = .fetched(.failure(.error("Malformed API URL."))) + return + } + + URLSession.shared.dataTask(with: apiUrl) { [weak self] (data, _, error) in + if let error = error { + self?.state = .fetched(.failure(.error(error.localizedDescription))) + return + } + + guard let data = data else { + self?.state = .fetched(.failure(.error("Malformed response data"))) + return + } + let root = try! JSONDecoder().decode(Root.self, from: data) + + DispatchQueue.main.async { [weak self] in + self?.state = .fetched(.success(root)) + } + }.resume() + } +} diff --git a/JuickNext/MessageView.swift b/JuickNext/MessageView.swift new file mode 100644 index 0000000..398978b --- /dev/null +++ b/JuickNext/MessageView.swift @@ -0,0 +1,43 @@ +// +// MessageView.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import SwiftUI + +struct MessageView: View { + var message: Message + var body: some View { + VStack(alignment: .leading) { + HStack { + LoadableImageView(with: message.user.avatar ?? "") + .frame(width: 48, height: 48, alignment: .center) + Text(message.user.name) + .font(.headline) + .foregroundColor(.accentColor) + } + message.tags.map { + Text($0.joined(separator: ", ")) + .font(.subheadline) + .italic() + .foregroundColor(.secondary) + } + Text(message.text ?? "") + .font(.body) + .padding() + message.attachment.map { + LoadableImageView(with: $0.url).scaledToFit() + } + } + } +} + +struct MessageView_Previews: PreviewProvider { + static let msg = Message(id: 0, user: User(id: 0, name: "ugnich"), text: "Lorem ipsum") + static var previews: some View { + MessageView(message: msg) + } +} diff --git a/JuickNext/Models.swift b/JuickNext/Models.swift new file mode 100644 index 0000000..77a12f5 --- /dev/null +++ b/JuickNext/Models.swift @@ -0,0 +1,47 @@ +// +// Models.swift +// tst +// +// Created by Vitaly Takmazov on 10.12.2019. +// Copyright © 2019 com.juick. All rights reserved. +// + +import Foundation + + +typealias Model = Decodable & Identifiable + +typealias Root = Array + +struct Attachment: Decodable { + var url: String +} + +struct Entity: Decodable { + var type: String + var text: String + var link: String? + var start: Int? + var end: Int? +} + +struct User: Model { + var id: Int + var name: String + var avatar: String? + private enum CodingKeys : String, CodingKey { + case id = "uid", name = "uname", avatar + } +} + +struct Message : Model { + var id: Int + var user: User + var text : String? + var attachment: Attachment? + var entities: [Entity]? + var tags: [String]? + private enum CodingKeys : String, CodingKey { + case id = "mid", user, text = "body", attachment, entities, tags + } +} diff --git a/JuickNext/View/ContentView.swift b/JuickNext/View/ContentView.swift index e77d6e7..22e74e0 100644 --- a/JuickNext/View/ContentView.swift +++ b/JuickNext/View/ContentView.swift @@ -10,17 +10,22 @@ import SwiftUI struct ContentView: View { @Environment(\.horizontalSizeClass) private var size + + let today = FeedView("Today", url: "https://api.juick.com/messages?popular=1") + let discussions = FeedView("Discussions", url: "https://api.juick.com/messages/discussions") + let discover = FeedView("Discover", url: "https://api.juick.com/messages") var body: some View { let view = (size == .compact) ? AnyView(TabView { - Today() - .tabItem { + today.tabItem { Image("ei-clock") } - Discussions() - .tabItem { + discussions.tabItem { Image("ei-bell") } + discover.tabItem { + Image("Discover") + } }): AnyView(NavigationView { VStack { Text("Discussions") diff --git a/JuickNext/View/Discussions.swift b/JuickNext/View/Discussions.swift deleted file mode 100644 index 20e7fa8..0000000 --- a/JuickNext/View/Discussions.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Discussions.swift -// Juick -// -// Created by Vitaly Takmazov on 14.05.2023. -// Copyright © 2023 com.juick. All rights reserved. -// - -import SwiftUI - -struct Discussions: View { - var body: some View { - Text("Discussions") - } -} - -struct Discussions_Previews: PreviewProvider { - static var previews: some View { - Discussions() - } -} diff --git a/JuickNext/View/Today.swift b/JuickNext/View/Today.swift deleted file mode 100644 index 161a353..0000000 --- a/JuickNext/View/Today.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TodayView.swift -// Juick -// -// Created by Vitaly Takmazov on 14.05.2023. -// Copyright © 2023 com.juick. All rights reserved. -// - -import SwiftUI - -struct Today: View { - let title = "Today" - var body: some View { - NavigationView { - Text(title) - } - } -} - -struct Today_Previews: PreviewProvider { - static var previews: some View { - Today() - } -} -- cgit v1.2.3