Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usecase,Entities,FeedListRepository,FirebaseRemoteDataSource를 생성했습니다. #2

Closed
wants to merge 10 commits into from
Closed
120 changes: 110 additions & 10 deletions HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@
/* Begin PBXBuildFile section */
1D2C16E62BE532B700C04508 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E52BE532B700C04508 /* AppDelegate.swift */; };
1D2C16E82BE532B700C04508 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E72BE532B700C04508 /* SceneDelegate.swift */; };
1D2C16EA2BE532B700C04508 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E92BE532B700C04508 /* ViewController.swift */; };
1D2C16EF2BE532B800C04508 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1D2C16EE2BE532B800C04508 /* Assets.xcassets */; };
1D2C16F22BE532B800C04508 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1D2C16F02BE532B800C04508 /* LaunchScreen.storyboard */; };
1D2C16FD2BE532B800C04508 /* HomeCafeRecipesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16FC2BE532B800C04508 /* HomeCafeRecipesTests.swift */; };
1D2C17072BE532B800C04508 /* HomeCafeRecipesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C17062BE532B800C04508 /* HomeCafeRecipesUITests.swift */; };
1D2C17092BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C17082BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift */; };
1D2E033E2BE913E500294417 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 1D2E033D2BE913E500294417 /* FirebaseAnalytics */; };
1DCB3D302BE9126E0036D305 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1DCB3D2F2BE9126E0036D305 /* GoogleService-Info.plist */; };
1D8262362C08A227000E981A /* FetchFeedListUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8262352C08A227000E981A /* FetchFeedListUsecase.swift */; };
1D8262382C08A233000E981A /* SearchFeedListUsecase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8262372C08A233000E981A /* SearchFeedListUsecase.swift */; };
1D82623A2C08A266000E981A /* FeedListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8262392C08A266000E981A /* FeedListViewController.swift */; };
1D82623C2C08BB10000E981A /* FeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D82623B2C08BB10000E981A /* FeedListRepository.swift */; };
1D82623E2C08BBB2000E981A /* FirebaseRemoteDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D82623D2C08BBB2000E981A /* FirebaseRemoteDataSource.swift */; };
1DBBFB752BF510D400524549 /* FeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBBFB742BF510D400524549 /* FeedItem.swift */; };
1DE852962BEF83DA0023AA96 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1DE852952BEF83DA0023AA96 /* GoogleService-Info.plist */; };
1DE8529E2BEFAB270023AA96 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 1DE8529D2BEFAB270023AA96 /* FirebaseFirestore */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -40,16 +46,20 @@
1D2C16E22BE532B700C04508 /* HomeCafeRecipes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HomeCafeRecipes.app; sourceTree = BUILT_PRODUCTS_DIR; };
1D2C16E52BE532B700C04508 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
1D2C16E72BE532B700C04508 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
1D2C16E92BE532B700C04508 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
1D2C16EE2BE532B800C04508 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
1D2C16F12BE532B800C04508 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
1D2C16F32BE532B800C04508 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1D2C16F82BE532B800C04508 /* HomeCafeRecipesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HomeCafeRecipesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
1D2C16FC2BE532B800C04508 /* HomeCafeRecipesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCafeRecipesTests.swift; sourceTree = "<group>"; };
1D2C17022BE532B800C04508 /* HomeCafeRecipesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HomeCafeRecipesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
1D2C17062BE532B800C04508 /* HomeCafeRecipesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCafeRecipesUITests.swift; sourceTree = "<group>"; };
1D2C17082BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCafeRecipesUITestsLaunchTests.swift; sourceTree = "<group>"; };
1DCB3D2F2BE9126E0036D305 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
1D8262352C08A227000E981A /* FetchFeedListUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFeedListUsecase.swift; sourceTree = "<group>"; };
1D8262372C08A233000E981A /* SearchFeedListUsecase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedListUsecase.swift; sourceTree = "<group>"; };
1D8262392C08A266000E981A /* FeedListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewController.swift; sourceTree = "<group>"; };
1D82623B2C08BB10000E981A /* FeedListRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListRepository.swift; sourceTree = "<group>"; };
1D82623D2C08BBB2000E981A /* FirebaseRemoteDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseRemoteDataSource.swift; sourceTree = "<group>"; };
1DBBFB742BF510D400524549 /* FeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItem.swift; sourceTree = "<group>"; };
1DE852952BEF83DA0023AA96 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -58,6 +68,7 @@
buildActionMask = 2147483647;
files = (
1D2E033E2BE913E500294417 /* FirebaseAnalytics in Frameworks */,
1DE8529E2BEFAB270023AA96 /* FirebaseFirestore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -85,6 +96,7 @@
1D2C16FB2BE532B800C04508 /* HomeCafeRecipesTests */,
1D2C17052BE532B800C04508 /* HomeCafeRecipesUITests */,
1D2C16E32BE532B700C04508 /* Products */,
1DE8529C2BEFAB270023AA96 /* Frameworks */,
);
sourceTree = "<group>";
};
Expand All @@ -101,13 +113,14 @@
1D2C16E42BE532B700C04508 /* HomeCafeRecipes */ = {
isa = PBXGroup;
children = (
1DCB3D2F2BE9126E0036D305 /* GoogleService-Info.plist */,
1D82622C2C0857BF000E981A /* Presentation */,
1D82622D2C0857CE000E981A /* Domain */,
1D82622E2C0857E1000E981A /* Data */,
1DE852952BEF83DA0023AA96 /* GoogleService-Info.plist */,
1D2C16E52BE532B700C04508 /* AppDelegate.swift */,
1D2C16E72BE532B700C04508 /* SceneDelegate.swift */,
1D2C16E92BE532B700C04508 /* ViewController.swift */,
1D2C16EE2BE532B800C04508 /* Assets.xcassets */,
1D2C16F02BE532B800C04508 /* LaunchScreen.storyboard */,
1D2C16F32BE532B800C04508 /* Info.plist */,
);
path = HomeCafeRecipes;
sourceTree = "<group>";
Expand All @@ -129,6 +142,80 @@
path = HomeCafeRecipesUITests;
sourceTree = "<group>";
};
1D82622C2C0857BF000E981A /* Presentation */ = {
isa = PBXGroup;
children = (
1DE852972BEF83E50023AA96 /* FeedList */,
);
path = Presentation;
sourceTree = "<group>";
};
1D82622D2C0857CE000E981A /* Domain */ = {
isa = PBXGroup;
children = (
1D8262322C08A1D1000E981A /* UseCase */,
1D8262312C08A1C4000E981A /* Entities */,
);
path = Domain;
sourceTree = "<group>";
};
1D82622E2C0857E1000E981A /* Data */ = {
isa = PBXGroup;
children = (
1D82623B2C08BB10000E981A /* FeedListRepository.swift */,
1D82623D2C08BBB2000E981A /* FirebaseRemoteDataSource.swift */,
);
path = Data;
sourceTree = "<group>";
};
1D82622F2C08586F000E981A /* View */ = {
isa = PBXGroup;
children = (
1D8262392C08A266000E981A /* FeedListViewController.swift */,
);
path = View;
sourceTree = "<group>";
};
1D8262302C085874000E981A /* ViewModel */ = {
isa = PBXGroup;
children = (
);
path = ViewModel;
sourceTree = "<group>";
};
1D8262312C08A1C4000E981A /* Entities */ = {
isa = PBXGroup;
children = (
1DBBFB742BF510D400524549 /* FeedItem.swift */,
);
path = Entities;
sourceTree = "<group>";
};
1D8262322C08A1D1000E981A /* UseCase */ = {
isa = PBXGroup;
children = (
1D8262372C08A233000E981A /* SearchFeedListUsecase.swift */,
1D8262352C08A227000E981A /* FetchFeedListUsecase.swift */,
);
path = UseCase;
sourceTree = "<group>";
};
1DE852972BEF83E50023AA96 /* FeedList */ = {
isa = PBXGroup;
children = (
1D8262302C085874000E981A /* ViewModel */,
1D82622F2C08586F000E981A /* View */,
);
path = FeedList;
sourceTree = "<group>";
};
1DE8529C2BEFAB270023AA96 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand All @@ -147,6 +234,7 @@
name = HomeCafeRecipes;
packageProductDependencies = (
1D2E033D2BE913E500294417 /* FirebaseAnalytics */,
1DE8529D2BEFAB270023AA96 /* FirebaseFirestore */,
);
productName = HomeCafeRecipes;
productReference = 1D2C16E22BE532B700C04508 /* HomeCafeRecipes.app */;
Expand Down Expand Up @@ -240,7 +328,7 @@
buildActionMask = 2147483647;
files = (
1D2C16F22BE532B800C04508 /* LaunchScreen.storyboard in Resources */,
1DCB3D302BE9126E0036D305 /* GoogleService-Info.plist in Resources */,
1DE852962BEF83DA0023AA96 /* GoogleService-Info.plist in Resources */,
1D2C16EF2BE532B800C04508 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -266,9 +354,14 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1D2C16EA2BE532B700C04508 /* ViewController.swift in Sources */,
1D82623C2C08BB10000E981A /* FeedListRepository.swift in Sources */,
1D82623E2C08BBB2000E981A /* FirebaseRemoteDataSource.swift in Sources */,
1D2C16E62BE532B700C04508 /* AppDelegate.swift in Sources */,
1D2C16E82BE532B700C04508 /* SceneDelegate.swift in Sources */,
1D8262382C08A233000E981A /* SearchFeedListUsecase.swift in Sources */,
1D8262362C08A227000E981A /* FetchFeedListUsecase.swift in Sources */,
1D82623A2C08A266000E981A /* FeedListViewController.swift in Sources */,
1DBBFB752BF510D400524549 /* FeedItem.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -454,6 +547,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = "-Wl,-ld_classic";
PRODUCT_BUNDLE_IDENTIFIER = GeonH0.HomeCafeRecipes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -481,6 +575,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = "-Wl,-ld_classic";
PRODUCT_BUNDLE_IDENTIFIER = GeonH0.HomeCafeRecipes;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -623,6 +718,11 @@
package = 1DCB3D312BE913880036D305 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseAnalytics;
};
1DE8529D2BEFAB270023AA96 /* FirebaseFirestore */ = {
isa = XCSwiftPackageProductDependency;
package = 1DCB3D312BE913880036D305 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseFirestore;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 1D2C16DA2BE532B700C04508 /* Project object */;
Expand Down
30 changes: 30 additions & 0 deletions HomeCafeRecipes/HomeCafeRecipes/Data/FeedListRepository.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// FeedListRepository.swift
// HomeCafeRecipes
//
// Created by 김건호 on 5/30/24.
//

import Foundation

protocol FeedListRepository {
func fetchFeedItems(completion: @escaping (Result<[FeedItem], Error>) -> Void)
func searchFeedItems(title: String, completion: @escaping (Result<[FeedItem], Error>) -> Void)
}
class FeedListRepositoryImpl: FeedListRepository {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개행 부탁드리구요, implement 클래스는 final로 선언하는게 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23aebe4 수정했습니다!

private let remoteDataSource: FirebaseRemoteDataSource

init(remoteDataSource: FirebaseRemoteDataSource) {
self.remoteDataSource = remoteDataSource
}

func fetchFeedItems(completion: @escaping (Result<[FeedItem], Error>) -> Void) {
remoteDataSource.fetchFeedItems(completion: completion)
}

func searchFeedItems(title : String, completion: @escaping (Result<[FeedItem], Error>) -> Void){
remoteDataSource.searchFeedItems(title: title, completion: completion)
}


Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

불필요한 개행인 것 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23aebe4 수정했습니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// FirebaseRemoteDataSource.swift
// HomeCafeRecipes
//
// Created by 김건호 on 5/30/24.
//

import FirebaseFirestore

class FirebaseRemoteDataSource {
private let db = Firestore.firestore()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더 명확한 변수 네이밍이면 좋을 것 같아요.


func fetchFeedItems(completion: @escaping (Result<[FeedItem], Error>) -> Void) {
db.collection("feedItems").getDocuments { (querySnapshot, error) in
if let error = error {
completion(.failure(error))
return
}
guard let documents = querySnapshot?.documents else {
completion(.success([]))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documents가 없어도 성공인건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성공은 했지만 빈배열일 경우를 생각해서 넣었습니다!

return
}
let feedItems = documents.compactMap { doc -> FeedItem? in
let data = doc.data()
guard
let title = data["title"] as? String,
let imageURL = data["imageURL"] as? [String] else {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배열이 반환되는 거면 imageURLs 와 같이 복수형으로 네이밍하는게 좋을 것 같아요

return nil
}
return FeedItem(id: doc.documentID, title: title, imageURL: imageURL)
Copy link

@f-lab-barry f-lab-barry Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문) document 모델은 어디서 정의하고 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 document는 파이어 베이스에서 제공하는 문서 자체의 ID를 선언했습니다. 하지만 해당 데이터의 id를 넣어야 할거 같아 수정하겠습니다!

}
completion(.success(feedItems))
}
}

func searchFeedItems(title: String, completion: @escaping (Result<[FeedItem], Error>) -> Void) {
db.collection("feedItems").getDocuments { (querySnapshot, error) in
if let error = error {
completion(.failure(error))
return
}
guard let documents = querySnapshot?.documents else {
completion(.success([]))
return
}
let feedItems = documents.compactMap { doc -> FeedItem? in
let data = doc.data()
guard
let title = data["title"] as? String,
let imageURL = data["imageURL"] as? [String],
title.contains(title) else {
return nil
}
return FeedItem(id: doc.documentID, title: title, imageURL: imageURL)
}
completion(.success(feedItems))
}
}
}


12 changes: 12 additions & 0 deletions HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/FeedItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// FeedModel.swift
// HomeCafeRecipes
//
// Created by 김건호 on 5/16/24.
//

struct FeedItem {
let id: String
let title : String
let imageURL : [String]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let imageURL : [String]
let imageURLs : [String]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// fetchFeedListUsecase.swift
// HomeCafeRecipes
//
// Created by 김건호 on 5/30/24.
//


protocol FetchFeedListUseCase {
func execute(completion: @escaping (Result<[FeedItem], Error>) -> Void)
}

class DefaultFetchFeedListUseCase: FetchFeedListUseCase {
private let repository: FeedListRepository

init(repository: FeedListRepository) {
self.repository = repository
}

func execute(completion: @escaping (Result<[FeedItem], Error>) -> Void) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FeedItem이 아닌 새로 정의한 도메인 Recipe로 받도록 변경해야할 것 같아요~

repository.fetchFeedItems { result in
completion(result)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// SearchFeedListusecase.swift
// HomeCafeRecipes
//
// Created by 김건호 on 5/30/24.
//


protocol SearchFeedListUseCase {
func execute(title: String, completion: @escaping (Result<[FeedItem], Error>) -> Void)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UseCase에서도 return 없이 Result 타입 completion 으로 처리하는 이유가 있을까요?
(비즈니스 로직 성공/실패는 어디서 핸들링하게 되는걸까요?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usecase에서도 FeedRlistepository에서 데이터를 가지고 받으면서 비동기 작업이 필요할거라 생각해서 completion으로 처리하였습니다!
비즈니스 로직 성공/실패는 viewmodel에서 핸들링 하려고 합니다!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • viewModel이 너무 많은 역할을 하게 되는 것, 뚱뚱해지는 것 방지
  • 비즈니스 로직 재사용성을 높임
  • SearchFeedListXXX에 대한 응집도 높임

}

class DefaultSearchFeedListUseCase: SearchFeedListUseCase {
private let repository: FeedListRepository

init(repository: FeedListRepository) {
self.repository = repository
}

func execute(title: String, completion: @escaping (Result<[FeedItem], Error>) -> Void) {
repository.searchFeedItems(title: title) { result in
completion(result)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// FeedListViewcontroller.swift
// HomeCafeRecipes
//
// Created by 김건호 on 5/30/24.
//

import UIKit

class FeedListViewController : UIViewController {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final이 붙어도 좋을까요? (: 옆에 띄어쓰기도 신경써주세요~)

Suggested change
class FeedListViewController : UIViewController {
final class FeedListViewController: UIViewController {


}
Loading
Loading