) {
18 | self.tag = tag
19 | self._selected = state
20 | }
21 |
22 | public var body: some View {
23 | Button(action: { selected = tag }) {
24 | Text(tag)
25 | }
26 | .modifier(TagFilterStyle())
27 | .background(Color.background(by: selected == tag))
28 | .disabled(selected == tag)
29 | }
30 | }
31 |
32 | // MARK: - Previews
33 |
34 | struct TagFilter_Previews: PreviewProvider {
35 |
36 | static var previews: some View {
37 | Group {
38 | TagFilter(tag: "performance", state: .constant("123"))
39 | .padding()
40 | .previewLayout(.sizeThatFits)
41 | .environment(\.colorScheme, .light)
42 |
43 | TagFilter(tag: "performance", state: .constant("performance"))
44 | .padding()
45 | .previewLayout(.sizeThatFits)
46 | .environment(\.colorScheme, .dark)
47 | }
48 | }
49 | }
50 |
51 | // MARK: - View Modifiers
52 |
53 | fileprivate struct TagFilterStyle: ViewModifier {
54 |
55 | func body(content: Content) -> some View {
56 | content
57 | .buttonStyle(BorderlessButtonStyle())
58 | .font(.system(size: 12, weight: .regular))
59 | .lineLimit(1)
60 | .foregroundColor(.foreground)
61 | .padding([.top, .bottom], 4.5)
62 | .padding([.leading, .trailing], 10)
63 | .cornerRadius(6)
64 | }
65 | }
66 |
67 | // MARK: - Colors
68 |
69 | fileprivate extension Color {
70 |
71 | static let foreground = Color.white
72 | static let background = Color.white.opacity(0.1)
73 | static func background(by selected: Bool) -> Color {
74 | selected ? Palette.main : .background
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Common/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | import class Foundation.ProcessInfo
6 |
7 | let package = Package(
8 | name: "Common",
9 | platforms: [.iOS(.v14)],
10 | products: [
11 | .library(name: "Common", targets: ["Common"])
12 | ],
13 | dependencies: Package.dependencies,
14 | targets: [
15 | .target(name: "Common", dependencies: Target.dependencies),
16 | .testTarget(name: "CommonTests", dependencies: ["Common"])
17 | ]
18 | )
19 |
20 | fileprivate extension Package {
21 |
22 | static var firebaseIsEnable: Bool {
23 | ProcessInfo.processInfo.environment["FIREBASE_DISABLED"] == nil
24 | }
25 | }
26 |
27 | fileprivate extension Target {
28 |
29 | static var dependencies: [Dependency] {
30 | var packages: [Dependency] = ["Introspect", "DataTransferObjects", "Errors", "Kingfisher"]
31 | if Package.firebaseIsEnable {
32 | packages.append(.product(name: "FirebaseCrashlytics", package: "Firebase"))
33 | }
34 | return packages
35 | }
36 | }
37 |
38 | fileprivate extension Package {
39 |
40 | static var dependencies: [Dependency] {
41 | internalDependencies + externalDependencies
42 | }
43 |
44 | static var internalDependencies: [Dependency] {
45 | return [
46 | .package(name: "DataTransferObjects", path: "../DataTransferObjects"),
47 | .package(path: "../Errors")
48 | ]
49 | }
50 |
51 | static var externalDependencies: [Dependency] {
52 | var packages: [Dependency] = [
53 | .package(name: "Introspect", url: "https://github.com/Puasonych/SwiftUI-Introspect.git", .branch("develop")),
54 | .package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher.git", .upToNextMajor(from: "6.0.0"))
55 | ]
56 | if Package.firebaseIsEnable {
57 | packages.append(
58 | .package(name: "Firebase", url: "https://github.com/firebase/firebase-ios-sdk.git", .upToNextMajor(from: "7.3.0"))
59 | )
60 | }
61 | return packages
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | StackOv
8 |
9 |
10 |
11 | A SwiftUI Stackoverflow client
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | We are currently in the developing process of the next version of StackOv app.
28 |
29 | The demo version of StackOv is available in the [AppStore](https://apps.apple.com/gb/app/stackov/id1511838391) as just a small StackOverflow reader.
30 |
31 | ## **Building**
32 |
33 | - [SwiftGen](https://github.com/SwiftGen/SwiftGen) is the necessary dependency of this project. If you have `homebrew` installed then just call `brew install swiftgen`.
34 | - Open the project and build the `StackOv (iOS)` target. 🙃
35 |
36 | ## **Contributing**
37 |
38 | We have two types of issue templates. Use the `feature request`, if you are ready to clarify your idea or task; use the `bug report`, if you want to provide a bug.
39 |
40 | Don't hesitate to discuss with us your ideas and to ask questions, we have [the discussion room](https://github.com/surfstudio/StackOv/discussions/categories/ideas) for this. 😌
41 | If you are ready to start work on some issue then go ahead, please just find the [DEVPROCESS.md](https://github.com/surfstudio/StackOv/blob/develop/DEVPROCESS.md) for your consideration.
42 |
--------------------------------------------------------------------------------
/AppScript/Sources/AppScript/Stores.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Stores.swift
3 | // StackOv (AppScript module)
4 | //
5 | // Created by Erik Basargin
6 | // Copyright © 2021 Erik Basargin. All rights reserved.
7 | //
8 |
9 | import Swinject
10 | import StackexchangeNetworkService
11 |
12 | @_exported import GlobalBannerStore
13 | @_exported import PageStore
14 | @_exported import FilterStore
15 | @_exported import ThreadStore
16 | @_exported import FavoriteStore
17 | @_exported import SidebarStore
18 | @_exported import CommentsStore
19 |
20 | // MARK: - Stores Assembly
21 |
22 | final class StoresAssembly: Assembly {
23 |
24 | func assemble(container: Container) {
25 | container.register(GlobalBannerStore.self) { resolver in
26 | GlobalBannerStore()
27 | }.inObjectScope(.weak)
28 |
29 | container.register(PageStore.self) { resolver in
30 | PageStore(dataManager: resolver.resolve(PageDataManager.self)!,
31 | filterStore: resolver.resolve(FilterStore.self)!)
32 | }.inObjectScope(.transient)
33 |
34 | container.register(ThreadStore.self) { resolver, model in
35 | ThreadStore(model: model, dataManager: resolver.resolve(ThreadDataManager.self)!)
36 | }.inObjectScope(.transient)
37 |
38 | container.register(FilterStore.self) { resolver in
39 | FilterStore()
40 | }.inObjectScope(.transient)
41 |
42 | container.register(SidebarStore.self) { resolver in
43 | SidebarStore()
44 | }.inObjectScope(.weak)
45 |
46 | container.register(FavoriteStore.self) { resolver in
47 | FavoriteStore(dataManager: resolver.resolve(FavoriteDataManager.self)!)
48 | }.inObjectScope(.transient)
49 |
50 | container.register(CommentsStore.self) { resolver, model in
51 | CommentsStore(model: model)
52 | }.inObjectScope(.transient)
53 | }
54 | }
55 |
56 | // MARK: - Stores Assembler
57 |
58 | public struct StoresAssembler {
59 |
60 | public static var shared: Resolver {
61 | assembler.resolver
62 | }
63 |
64 | public static let assembler: Assembler = {
65 | Assembler([
66 | StoresAssembly()
67 | ], parent: ServicesAssembler.assembler)
68 | }()
69 | }
70 |
--------------------------------------------------------------------------------
/Stores/FavoriteStore/Sources/FavoriteStore/FavoriteStore.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteStore.swift
3 | // StackOv (FavoriteStore module)
4 | //
5 | // Created by Erik Basargin
6 | // Copyright © 2021 Erik Basargin. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | import StackexchangeNetworkService
12 | import Common
13 |
14 | public final class FavoriteStore: ObservableObject {
15 |
16 | // MARK: - Nested types
17 |
18 | public enum State {
19 | case unknown
20 | case emptyContent
21 | case content([QuestionModel])
22 | case loading
23 | case error(Error)
24 | }
25 |
26 | // MARK: - Substores & Services
27 |
28 | let dataManager: FavoriteDataManagerProtocol
29 |
30 | // MARK: - Public properties
31 |
32 | @Published public private(set) var state: State = .unknown
33 | @Published public private(set) var loadMore: Bool = false
34 |
35 | // MARK: - Initialization and deinitialization
36 |
37 | public init(dataManager: FavoriteDataManagerProtocol) {
38 | self.dataManager = dataManager
39 | }
40 | }
41 |
42 | // MARK: - Actions
43 |
44 | public extension FavoriteStore {
45 |
46 | func loadNextQuestions() {
47 | if case .loading = state { return }
48 | loadMore = true
49 | dataManager.fetch { [unowned self] result in
50 | loadMore = false
51 | switch result {
52 | case let .success(models):
53 | if models.isEmpty { break }
54 | state = .content(models)
55 | case .failure:
56 | // need show error not by changing the state of screen
57 | break
58 | }
59 | }
60 | }
61 |
62 | func reloadQuestions() {
63 | loadMore = false
64 | state = .loading
65 | dataManager.reload { [unowned self] result in
66 | switch result {
67 | case let .success(models):
68 | state = models.isEmpty ? .emptyContent : .content(models)
69 | case .failure:
70 | switch state {
71 | case .emptyContent:
72 | // need show error state of screen
73 | break
74 | default:
75 | // need show error not by changing the state of screen
76 | break
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Application/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | LSApplicationCategoryType
22 | public.app-category.social-networking
23 | LSRequiresIPhoneOS
24 |
25 | NSAppTransportSecurity
26 |
27 | NSAllowsArbitraryLoads
28 |
29 | NSExceptionDomains
30 |
31 | api.stackexchange.com
32 |
33 | NSExceptionAllowsInsecureHTTPLoads
34 |
35 | NSIncludesSubdomains
36 |
37 |
38 |
39 |
40 | NSHumanReadableCopyright
41 | Copyright © 2021 Erik Basargin. All rights reserved.
42 | UIApplicationSceneManifest
43 |
44 | UIApplicationSupportsMultipleScenes
45 |
46 |
47 | UIApplicationSupportsIndirectInputEvents
48 |
49 | UILaunchScreen
50 |
51 | UIRequiredDeviceCapabilities
52 |
53 | armv7
54 |
55 | UISupportedInterfaceOrientations
56 |
57 | UIInterfaceOrientationPortrait
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 | UISupportedInterfaceOrientations~ipad
62 |
63 | UIInterfaceOrientationPortrait
64 | UIInterfaceOrientationPortraitUpsideDown
65 | UIInterfaceOrientationLandscapeLeft
66 | UIInterfaceOrientationLandscapeRight
67 |
68 | NSPhotoLibraryAddUsageDescription
69 | StackOv needs permission to access photos on your device
70 |
71 |
72 |
--------------------------------------------------------------------------------
/Icons/Sources/Icons/Icons.xcassets/paperclip.imageset/paperclip.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | # OS X Finder stuff
6 | .DS_Store
7 |
8 | ## User settings
9 | xcuserdata/
10 |
11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
12 | *.xcscmblueprint
13 | *.xccheckout
14 |
15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
16 | build/
17 | DerivedData/
18 | *.moved-aside
19 | *.pbxuser
20 | !default.pbxuser
21 | *.mode1v3
22 | !default.mode1v3
23 | *.mode2v3
24 | !default.mode2v3
25 | *.perspectivev3
26 | !default.perspectivev3
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 |
31 | ## App packaging
32 | *.ipa
33 | *.dSYM.zip
34 | *.dSYM
35 |
36 | ## Playgrounds
37 | timeline.xctimeline
38 | playground.xcworkspace
39 |
40 | # Swift Package Manager
41 | #
42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
43 | # Packages/
44 | # Package.pins
45 | # Package.resolved
46 | # *.xcodeproj
47 | #
48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
49 | # hence it is not needed unless you have added a package configuration file to your project
50 | # .swiftpm
51 |
52 | .build/
53 |
54 | # CocoaPods
55 | #
56 | # We recommend against adding the Pods directory to your .gitignore. However
57 | # you should judge for yourself, the pros and cons are mentioned at:
58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
59 | #
60 | # Pods/
61 | #
62 | # Add this line if you want to avoid checking in source code from the Xcode workspace
63 | # *.xcworkspace
64 |
65 | # Carthage
66 | #
67 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
68 | # Carthage/Checkouts
69 |
70 | Carthage/Build/
71 |
72 | # Accio dependency management
73 | Dependencies/
74 | .accio/
75 |
76 | # fastlane
77 | #
78 | # It is recommended to not store the screenshots in the git repo.
79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
80 | # For more information about the recommended setup visit:
81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
82 |
83 | fastlane/report.xml
84 | fastlane/Preview.html
85 | fastlane/screenshots/**/*.png
86 | fastlane/test_output
87 |
88 | # Code Injection
89 | #
90 | # After new code Injection tools there's a generated folder /iOSInjectionProject
91 | # https://github.com/johnno1962/injectionforxcode
92 |
93 | iOSInjectionProject/
94 |
--------------------------------------------------------------------------------
/Icons/.swiftpm/xcode/xcshareddata/xcschemes/Icons.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------