diff --git a/Dear-World/Dear-World.xcodeproj/MessageCountBadgeView.swift b/Dear-World/Dear-World.xcodeproj/MessageCountBadgeView.swift index d976b48..0b4b7cc 100644 --- a/Dear-World/Dear-World.xcodeproj/MessageCountBadgeView.swift +++ b/Dear-World/Dear-World.xcodeproj/MessageCountBadgeView.swift @@ -5,14 +5,14 @@ // Created by rookie.w on 2020/12/26. // -import Then import SnapKit +import Then import UIKit final class MessageCountBadgeView: UIView { private var totalCount: UILabel = UILabel() - public var count: Int? { + var count: Int? { didSet { self.totalCount.text = count?.formatted } @@ -24,13 +24,6 @@ final class MessageCountBadgeView: UIView { setupSubviews() } -// override public func didMoveToSuperview() { -// self.snp.makeConstraints { -// $0.centerX.equalToSuperview() -// $0.top.equalToSuperview().inset(60) -// } -// } -// private func setupUI() { self.snp.makeConstraints { $0.width.equalTo(88) diff --git a/Dear-World/Dear-World.xcodeproj/project.pbxproj b/Dear-World/Dear-World.xcodeproj/project.pbxproj index 52ddbae..92073ed 100644 --- a/Dear-World/Dear-World.xcodeproj/project.pbxproj +++ b/Dear-World/Dear-World.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 121BDB592597652A0062B15A /* MessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121BDB582597652A0062B15A /* MessageTableViewCell.swift */; }; 121BDB672597982F0062B15A /* DiscoverReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121BDB662597982F0062B15A /* DiscoverReactor.swift */; }; 121BDB8E259829840062B15A /* CountrySelectController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 121BDB8D259829840062B15A /* CountrySelectController.swift */; }; - 1221913B25B19EB8001D0D10 /* heartfull.json in Resources */ = {isa = PBXBuildFile; fileRef = 1221913A25B19EB8001D0D10 /* heartfull.json */; }; 12257E1825A622750007E65E /* SortTypeSelectController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12257E1725A622750007E65E /* SortTypeSelectController.swift */; }; 1263E09725A1C87400E3F121 /* Message.API.Countries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1263E09625A1C87400E3F121 /* Message.API.Countries.swift */; }; 1263E09C25A1C8DD00E3F121 /* Message.Model.Countries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1263E09B25A1C8DD00E3F121 /* Message.Model.Countries.swift */; }; @@ -70,6 +69,13 @@ 3971EB32259A8BC90084E6DC /* SwiftRichString in Frameworks */ = {isa = PBXBuildFile; productRef = 3971EB31259A8BC90084E6DC /* SwiftRichString */; }; 3971EB37259A9C550084E6DC /* Message.API.SendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971EB36259A9C550084E6DC /* Message.API.SendMessage.swift */; }; 3971EB3C259A9C860084E6DC /* Message.Model.SendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971EB3B259A9C860084E6DC /* Message.Model.SendMessage.swift */; }; + 39965E6425ABAF7200069860 /* ItemBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39965E6325ABAF7200069860 /* ItemBottomSheetViewController.swift */; }; + 39965E6925ABCD1700069860 /* Promisable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39965E6825ABCD1700069860 /* Promisable.swift */; }; + 39965E7225ABD52400069860 /* BottomSheetItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39965E7125ABD52400069860 /* BottomSheetItemCell.swift */; }; + 39965E7A25ABDF8100069860 /* ItemBottomReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39965E7925ABDF8100069860 /* ItemBottomReactor.swift */; }; + 39965E8825ABE32400069860 /* BottomSheetItemHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39965E8725ABE32400069860 /* BottomSheetItemHeaderView.swift */; }; + 39965E8D25ACA87A00069860 /* BottomSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39965E8C25ACA87A00069860 /* BottomSheetItem.swift */; }; + 39965F2525B2E20A00069860 /* heart_fill.json in Resources */ = {isa = PBXBuildFile; fileRef = 39965F2425B2E20900069860 /* heart_fill.json */; }; 39C832282597651F00236DDF /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 39C832272597651F00236DDF /* RxRelay */; }; 39C8322A2597651F00236DDF /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 39C832292597651F00236DDF /* RxSwift */; }; 39C8322C2597651F00236DDF /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 39C8322B2597651F00236DDF /* RxCocoa */; }; @@ -116,7 +122,6 @@ 121BDB582597652A0062B15A /* MessageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewCell.swift; sourceTree = ""; }; 121BDB662597982F0062B15A /* DiscoverReactor.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = DiscoverReactor.swift; sourceTree = ""; tabWidth = 2; }; 121BDB8D259829840062B15A /* CountrySelectController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountrySelectController.swift; sourceTree = ""; }; - 1221913A25B19EB8001D0D10 /* heartfull.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = heartfull.json; path = ../../../../Downloads/heartfull.json; sourceTree = ""; }; 12257E1725A622750007E65E /* SortTypeSelectController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTypeSelectController.swift; sourceTree = ""; }; 1263E09625A1C87400E3F121 /* Message.API.Countries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.API.Countries.swift; sourceTree = ""; }; 1263E09B25A1C8DD00E3F121 /* Message.Model.Countries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.Model.Countries.swift; sourceTree = ""; }; @@ -166,6 +171,13 @@ 3971EB28259A7D720084E6DC /* SendMessageReactor.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = SendMessageReactor.swift; sourceTree = ""; tabWidth = 2; }; 3971EB36259A9C550084E6DC /* Message.API.SendMessage.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Message.API.SendMessage.swift; sourceTree = ""; tabWidth = 2; }; 3971EB3B259A9C860084E6DC /* Message.Model.SendMessage.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; name = Message.Model.SendMessage.swift; path = "Dear-World/Source/Domain/Message/API/Message.Model.SendMessage.swift"; sourceTree = SOURCE_ROOT; tabWidth = 2; }; + 39965E6325ABAF7200069860 /* ItemBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemBottomSheetViewController.swift; sourceTree = ""; }; + 39965E6825ABCD1700069860 /* Promisable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Promisable.swift; sourceTree = ""; }; + 39965E7125ABD52400069860 /* BottomSheetItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetItemCell.swift; sourceTree = ""; }; + 39965E7925ABDF8100069860 /* ItemBottomReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemBottomReactor.swift; sourceTree = ""; }; + 39965E8725ABE32400069860 /* BottomSheetItemHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetItemHeaderView.swift; sourceTree = ""; }; + 39965E8C25ACA87A00069860 /* BottomSheetItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetItem.swift; sourceTree = ""; }; + 39965F2425B2E20900069860 /* heart_fill.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = heart_fill.json; sourceTree = ""; }; 39E9F7CF25A1738F00BC2CC2 /* AboutReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutReactor.swift; sourceTree = ""; }; 39E9F7DA25A1BBED00BC2CC2 /* PixelWorldMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelWorldMapView.swift; sourceTree = ""; }; 39E9F7DF25A1BC3C00BC2CC2 /* NoticeBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeBadge.swift; sourceTree = ""; }; @@ -233,6 +245,7 @@ children = ( 3902F14F2597531600A3DF8C /* Extension */, 39EED240259D0227007452E1 /* Revision.swift */, + 39965E6825ABCD1700069860 /* Promisable.swift */, ); path = Common; sourceTree = ""; @@ -373,8 +386,8 @@ 395825AE25948E66007325AB /* Resource */ = { isa = PBXGroup; children = ( + 39965F2425B2E20900069860 /* heart_fill.json */, 39658F9F259ADE770050D180 /* splash_1x.json */, - 1221913A25B19EB8001D0D10 /* heartfull.json */, 395825B625948EE4007325AB /* Colors.xcassets */, 3958258325948E43007325AB /* Assets.xcassets */, 3958258525948E43007325AB /* LaunchScreen.storyboard */, @@ -422,6 +435,7 @@ 395825B425948EC6007325AB /* View */ = { isa = PBXGroup; children = ( + 39965E7025ABD4E400069860 /* BottomSheet */, 121BDB292597259E0062B15A /* MessageCountBadgeView.swift */, 39F465812597811900621327 /* CheerButton.swift */, 121BDB8D259829840062B15A /* CountrySelectController.swift */, @@ -559,6 +573,18 @@ path = API; sourceTree = ""; }; + 39965E7025ABD4E400069860 /* BottomSheet */ = { + isa = PBXGroup; + children = ( + 39965E7925ABDF8100069860 /* ItemBottomReactor.swift */, + 39965E6325ABAF7200069860 /* ItemBottomSheetViewController.swift */, + 39965E7125ABD52400069860 /* BottomSheetItemCell.swift */, + 39965E8725ABE32400069860 /* BottomSheetItemHeaderView.swift */, + 39965E8C25ACA87A00069860 /* BottomSheetItem.swift */, + ); + path = BottomSheet; + sourceTree = ""; + }; 39E9F7E425A1C78900BC2CC2 /* World Map */ = { isa = PBXGroup; children = ( @@ -742,7 +768,7 @@ 39518C7725A8FA2400F777D1 /* GoogleService-Info.plist in Resources */, 395825B725948EE4007325AB /* Colors.xcassets in Resources */, 3958258425948E43007325AB /* Assets.xcassets in Resources */, - 1221913B25B19EB8001D0D10 /* heartfull.json in Resources */, + 39965F2525B2E20A00069860 /* heart_fill.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -837,6 +863,8 @@ 395A4177259DF9F900F10531 /* MainTabBarController.swift in Sources */, 3971EB18259A7BC10084E6DC /* Emoji.swift in Sources */, 3902F1132597049D00A3DF8C /* SendMessageViewController.swift in Sources */, + 39965E6925ABCD1700069860 /* Promisable.swift in Sources */, + 39965E7A25ABDF8100069860 /* ItemBottomReactor.swift in Sources */, 3914D9E6259F3FA1009765B0 /* Message.Model.Messages.swift in Sources */, 39F0C1A02598877600A7001F /* NetworkError.swift in Sources */, 121BDB592597652A0062B15A /* MessageTableViewCell.swift in Sources */, @@ -862,12 +890,14 @@ 39F0C19B2598860100A7001F /* ResponseWrapper.swift in Sources */, 39E9F7E625A1C80300BC2CC2 /* WorldMap.swift in Sources */, 3971EB3C259A9C860084E6DC /* Message.Model.SendMessage.swift in Sources */, + 39965E8D25ACA87A00069860 /* BottomSheetItem.swift in Sources */, 39658FAF259AE4E40050D180 /* World.swift in Sources */, 3971EB24259A7C420084E6DC /* Emoji.Model.Random.swift in Sources */, 39F0C1632597B34F00A7001F /* UIView+.swift in Sources */, 3971EB1F259A7C0E0084E6DC /* Emoji.API.Random.swift in Sources */, 393E0D4025A23A12000DB3B9 /* World.Model.Map.swift in Sources */, 3902F1052596F26D00A3DF8C /* CheeringMapViewController.swift in Sources */, + 39965E6425ABAF7200069860 /* ItemBottomSheetViewController.swift in Sources */, 39F0C1B72598927C00A7001F /* ErrorView.swift in Sources */, 39672DC12598E95A001D7E69 /* TextView.swift in Sources */, 1263E09C25A1C8DD00E3F121 /* Message.Model.Countries.swift in Sources */, @@ -875,8 +905,10 @@ 121BDB2A2597259E0062B15A /* MessageCountBadgeView.swift in Sources */, 39F0C16B2597C7DF00A7001F /* UIImage+.swift in Sources */, 393E0D5725A2BE3C000DB3B9 /* AboutTeamViewController.swift in Sources */, + 39965E7225ABD52400069860 /* BottomSheetItemCell.swift in Sources */, 39E9F7E025A1BC3C00BC2CC2 /* NoticeBadge.swift in Sources */, 12257E1825A622750007E65E /* SortTypeSelectController.swift in Sources */, + 39965E8825ABE32400069860 /* BottomSheetItemHeaderView.swift in Sources */, 39E9F7ED25A1C84900BC2CC2 /* WorldMap.Model.Country.swift in Sources */, 39658FC0259AE58C0050D180 /* World.Model.Country.swift in Sources */, 121BDB8E259829840062B15A /* CountrySelectController.swift in Sources */, diff --git a/Dear-World/Dear-World/Resource/heart_fill.json b/Dear-World/Dear-World/Resource/heart_fill.json new file mode 100644 index 0000000..e25d26b --- /dev/null +++ b/Dear-World/Dear-World/Resource/heart_fill.json @@ -0,0 +1 @@ +{"v":"5.7.4","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":200,"h":200,"nm":"","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"모양 레이어 5","sr":1,"ks":{"o":{"a":1,"k":[{"t":8,"s":[0],"h":1},{"t":16,"s":[100],"h":1},{"t":27.0000010997325,"s":[0],"h":1}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[91.5,100,0],"ix":2,"l":2},"a":{"a":0,"k":[-5.937,1.587,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":16,"s":[126,126,100]},{"t":27.0000010997325,"s":[138,138,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8.426,8.426],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"타원 패스 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.050980392157,0.796078431373,0.737254901961,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"선 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.051799332862,0.794515931373,0.736900658701,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"칠 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-24.287,-58.287],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":8,"s":[18,18]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":14,"s":[54,54]},{"t":19.0000007738859,"s":[12,12]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"변형"}],"nm":"타원 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"rp","c":{"a":0,"k":9,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":2,"tr":{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":41,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"변형"},"nm":"반복 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":1.00000004073083,"op":61.0000024845809,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"모양 레이어 4","sr":1,"ks":{"o":{"a":1,"k":[{"t":7,"s":[0],"h":1},{"t":11,"s":[100],"h":1},{"t":20.0000008146167,"s":[0],"h":1}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[91.5,100,0],"ix":2,"l":2},"a":{"a":0,"k":[-5.937,1.587,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":11,"s":[111,111,100]},{"t":20.0000008146167,"s":[128,128,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8.426,8.426],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"타원 패스 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.980392156863,0.21568627451,0.4,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"선 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.980392156863,0.21568627451,0.4,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"칠 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-24.287,-58.287],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":7,"s":[18,18]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":11,"s":[80,80]},{"t":16.0000006516934,"s":[12,12]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"변형"}],"nm":"타원 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"rp","c":{"a":0,"k":9,"ix":1},"o":{"a":0,"k":0,"ix":2},"m":1,"ix":2,"tr":{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":40,"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"변형"},"nm":"반복 1","mn":"ADBE Vector Filter - Repeater","hd":false}],"ip":1.00000004073083,"op":61.0000024845809,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"heart","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100.5,100.75,0],"ix":2,"l":2},"a":{"a":0,"k":[51,43,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":12,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":17,"s":[80,80,100]},{"i":{"x":[0.571,0.571,0.571],"y":[1,1,1]},"o":{"x":[0.174,0.174,0.174],"y":[0,0,0]},"t":23,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.399,0.399,0.399],"y":[0,0,0]},"t":30,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":38,"s":[130,130,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":45,"s":[80,80,100]},{"t":55.0000022401959,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[5.491,5.707],[7.198,0],[5.087,-8.058],[9.076,0],[5.215,-5.417],[-0.307,-8.035],[-1.964,-1.007],[-0.266,0],[-0.243,0.126],[-11.932,9.608],[-0.47,12.312]],"o":[[-5.215,-5.417],[-9.074,0],[-5.085,-8.058],[-7.198,0],[-5.491,5.707],[1.11,29.108],[0.243,0.126],[0.266,0],[0.488,-0.25],[16.237,-13.073],[0.307,-8.035]],"v":[[41.819,-33.758],[22.568,-42.159],[0,-29.21],[-22.568,-42.159],[-41.819,-33.758],[-49.978,-12.135],[-0.778,41.972],[0,42.159],[0.778,41.972],[24.801,26.122],[49.978,-12.135]],"c":true},"ix":2},"nm":"패스 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.838999986649,0.866999983788,0.98400002718,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0.838999968884,0.866999966491,0.984000052658,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0.980392158031,0.215686276555,0.40000000596,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0.980392158031,0.215686276555,0.40000000596,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":38,"s":[0.839215695858,0.866666674614,0.984313726425,1]},{"t":45.0000018328876,"s":[0.839215695858,0.866666674614,0.984313726425,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"칠 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[51,42.604],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"변형"}],"nm":"그룹 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120.0000048877,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Dear-World/Dear-World/Source/Common/Extension/Number+.swift b/Dear-World/Dear-World/Source/Common/Extension/Number+.swift index e65e6dd..a140173 100644 --- a/Dear-World/Dear-World/Source/Common/Extension/Number+.swift +++ b/Dear-World/Dear-World/Source/Common/Extension/Number+.swift @@ -31,7 +31,7 @@ extension Int { extension Double { var decimalFormat: String? { let numberFormatter: NumberFormatter = NumberFormatter().then { - $0.roundingMode = .halfEven + $0.roundingMode = .down $0.minimumFractionDigits = 0 $0.maximumFractionDigits = 1 $0.numberStyle = .decimal diff --git a/Dear-World/Dear-World/Source/Common/Extension/UIView+.swift b/Dear-World/Dear-World/Source/Common/Extension/UIView+.swift index 600c498..3ebb0e1 100644 --- a/Dear-World/Dear-World/Source/Common/Extension/UIView+.swift +++ b/Dear-World/Dear-World/Source/Common/Extension/UIView+.swift @@ -8,14 +8,4 @@ import UIKit extension UIView { - func roundCorners(corners: UIRectCorner, radius: CGFloat) { - let path: UIBezierPath = UIBezierPath( - roundedRect: bounds, - byRoundingCorners: corners, - cornerRadii: CGSize(width: radius, height: radius) - ) - let mask: CAShapeLayer = CAShapeLayer() - mask.path = path.cgPath - layer.mask = mask - } } diff --git a/Dear-World/Dear-World/Source/Common/Promisable.swift b/Dear-World/Dear-World/Source/Common/Promisable.swift new file mode 100644 index 0000000..b3db1ff --- /dev/null +++ b/Dear-World/Dear-World/Source/Common/Promisable.swift @@ -0,0 +1,33 @@ +// +// Completable.swift +// Dear-World +// +// Created by dongyoung.lee on 2021/01/11. +// + +import Foundation +import RxCocoa +import RxSwift + +enum AssociatedKeys { + static var expectedValue: String = "expectedValue" +} + +protocol Promisable { + associatedtype Expected + var expected: PublishRelay { get } +} +extension Promisable where Self: AnyObject { + var expected: PublishRelay { + get { + if let object: PublishRelay = objc_getAssociatedObject(self, &AssociatedKeys.expectedValue) as? PublishRelay { + return object + } else { + let object: PublishRelay = PublishRelay() + self.expected = object + return object + } + } + set { objc_setAssociatedObject(self, &AssociatedKeys.expectedValue, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } +} diff --git a/Dear-World/Dear-World/Source/Common/Revision.swift b/Dear-World/Dear-World/Source/Common/Revision.swift index ff5f734..5454fb3 100644 --- a/Dear-World/Dear-World/Source/Common/Revision.swift +++ b/Dear-World/Dear-World/Source/Common/Revision.swift @@ -16,14 +16,13 @@ struct Revision: Equatable { } extension Revision { static func == (lhs: Revision, rhs: Revision) -> Bool { - let r1 = lhs.projectedValue == rhs.projectedValue - return r1 + lhs.projectedValue == rhs.projectedValue } } extension Revision where T: Equatable { static func == (lhs: Revision, rhs: Revision) -> Bool { - let r1 = lhs.projectedValue == rhs.projectedValue - let r2 = lhs.wrappedValue == rhs.wrappedValue + let r1: Bool = lhs.projectedValue == rhs.projectedValue + let r2: Bool = lhs.wrappedValue == rhs.wrappedValue return r1 && r2 } } diff --git a/Dear-World/Dear-World/Source/Core/AppDelegate.swift b/Dear-World/Dear-World/Source/Core/AppDelegate.swift index 63acfb2..a730ef0 100644 --- a/Dear-World/Dear-World/Source/Core/AppDelegate.swift +++ b/Dear-World/Dear-World/Source/Core/AppDelegate.swift @@ -4,11 +4,11 @@ // // Created by dongyoung.lee on 2020/12/24. // +import Firebase import RxSwift import SnapKit import Then import UIKit -import Firebase @main final class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/Dear-World/Dear-World/Source/Domain/Emoji/API/Emoji.API.Random.swift b/Dear-World/Dear-World/Source/Domain/Emoji/API/Emoji.API.Random.swift index 8f0e25f..0809432 100644 --- a/Dear-World/Dear-World/Source/Domain/Emoji/API/Emoji.API.Random.swift +++ b/Dear-World/Dear-World/Source/Domain/Emoji/API/Emoji.API.Random.swift @@ -14,6 +14,6 @@ extension Emoji.API { var method: HTTPMethod { .get } var path: String { "api/v1/emojis/random" } - var parameters: [String : Any?]? { nil } + var parameters: [String: Any?]? { nil } } } diff --git a/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Countries.swift b/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Countries.swift index 4715932..2fc407c 100644 --- a/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Countries.swift +++ b/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Countries.swift @@ -15,6 +15,5 @@ extension Message.API { var method: HTTPMethod { .get } var path: String { "api/v1/countries/" } var parameters: [String : Any?]? { nil } - } } diff --git a/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Messages.swift b/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Messages.swift index 5b72d25..8bc6f24 100644 --- a/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Messages.swift +++ b/Dear-World/Dear-World/Source/Domain/Message/API/Message.API.Messages.swift @@ -15,7 +15,7 @@ extension Message.API { // MARK: Parameters private let countryCode: String? private let lastMsgId: Int? - private let type: Message.Model.ListType + private let type: Message.Model.Sort var method: HTTPMethod { .get } var path: String { "/api/v1/messages" } @@ -27,7 +27,7 @@ extension Message.API { ] } - init(countryCode: String?, lastMsgId: Int?, type: Message.Model.ListType) { + init(countryCode: String?, lastMsgId: Int?, type: Message.Model.Sort) { self.countryCode = countryCode self.lastMsgId = lastMsgId self.type = type diff --git a/Dear-World/Dear-World/Source/Domain/World Map/Model/WorldMap.Model.Country.swift b/Dear-World/Dear-World/Source/Domain/World Map/Model/WorldMap.Model.Country.swift index 64ce9db..34560c5 100644 --- a/Dear-World/Dear-World/Source/Domain/World Map/Model/WorldMap.Model.Country.swift +++ b/Dear-World/Dear-World/Source/Domain/World Map/Model/WorldMap.Model.Country.swift @@ -1141,7 +1141,7 @@ extension WorldMap.Model { Location(x: 42, y: 6), Location(x: 42, y: 7), Location(x: 42, y: 9), - Location(x: 42, y: 15), + Location(x: 42, y: 15) ] ) static let SE: Country = Country( diff --git a/Dear-World/Dear-World/Source/Network/Network.swift b/Dear-World/Dear-World/Source/Network/Network.swift index b8050e6..5f0c1a2 100644 --- a/Dear-World/Dear-World/Source/Network/Network.swift +++ b/Dear-World/Dear-World/Source/Network/Network.swift @@ -9,14 +9,17 @@ import Alamofire import Foundation import RxSwift - enum Network { + static var isDebug: Bool = false + static func request(_ endpoint: API) -> Observable { .create { observer in AF.request(endpoint) .validate(statusCode: 200..<300) .responseDecodable(of: ResponseWrapper.self) { result in - Logger.log(result) + if isDebug { + Logger.log(result) + } switch result.result { case .success(let response): if let error: NetworkError = NetworkError(code: response.code, message: response.message) { diff --git a/Dear-World/Dear-World/Source/Network/ServiceAPI.swift b/Dear-World/Dear-World/Source/Network/ServiceAPI.swift index 39288aa..7acaeec 100644 --- a/Dear-World/Dear-World/Source/Network/ServiceAPI.swift +++ b/Dear-World/Dear-World/Source/Network/ServiceAPI.swift @@ -25,7 +25,7 @@ extension ServiceAPI { let url: URL = baseURL.appendingPathComponent(path) var request: URLRequest = URLRequest(url: url) request.httpMethod = method.rawValue - let parameters = self.parameters?.compactMapValues { $0 } + let parameters: [String: Any]? = self.parameters?.compactMapValues { $0 } switch method { case .post: request = try JSONEncoding.default.encode(request, with: parameters) diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapReactor.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapReactor.swift index 8b75fe5..8d1e187 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapReactor.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapReactor.swift @@ -29,8 +29,9 @@ final class CheeringMapReactor: Reactor { var messageCount: Int = 100_000 @Revision var rankers: [Model.Country] = [] @Revision var countries: [Model.Country] = [] - @Revision var selectedCountries: [Model.Country] = [] + @Revision var selectedCountries: Model.Country? @Revision var isPresentAboutPage: Bool = false + @Revision var isPresentFilter: Bool = false } var initialState: State = State() @@ -67,7 +68,7 @@ final class CheeringMapReactor: Reactor { var newState = currentState switch mutation { case .setPresentAboutPage(let isPresentAboutPage): - newState.isPresentAboutPage = true + newState.isPresentAboutPage = isPresentAboutPage case .setRankers(let countries): newState.rankers = countries @@ -84,3 +85,5 @@ final class CheeringMapReactor: Reactor { return newState } } + +extension World.Model.Country: BottomSheetItem {} diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapViewController.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapViewController.swift index fc0b248..bb79dfe 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapViewController.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Cheering Map/CheeringMapViewController.swift @@ -77,7 +77,8 @@ final class CheeringMapViewController: UIViewController, ReactorKit.View { reactor.state .distinctUntilChanged(\.$isPresentAboutPage) .map { $0.isPresentAboutPage } - .subscribe(onNext: { [weak self] in + .filter { $0 } + .subscribe(onNext: { [weak self] _ in let viewController = AboutViewController().then { $0.reactor = AboutReactor() } diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Discover/Cell/MessageTableViewCell.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Discover/Cell/MessageTableViewCell.swift index 68285e9..c7ac7fa 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Discover/Cell/MessageTableViewCell.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Discover/Cell/MessageTableViewCell.swift @@ -5,10 +5,10 @@ // Created by rookie.w on 2020/12/26. // +import Lottie import RxCocoa import RxSwift import UIKit -import Lottie final class MessageTableViewCell: UITableViewCell { private enum HeartProgress: CGFloat { @@ -30,7 +30,7 @@ final class MessageTableViewCell: UITableViewCell { let countryFlagImageView: UIImageView = UIImageView() let detailTextView: UITextView = UITextView() let shareButton: UIButton = UIButton() - let likeView: AnimationView = AnimationView(animation: Animation.named("heartfull")) + let likeView: AnimationView = AnimationView(animation: Animation.named("heart_fill")) let likeCountLabel: UILabel = UILabel() var likeCount: Int = 0 { didSet { @@ -72,8 +72,10 @@ final class MessageTableViewCell: UITableViewCell { } let mainView: UIView = UIView().then { $0.backgroundColor = .white - $0.layer.masksToBounds = true $0.layer.cornerRadius = 20 + $0.layer.shadowOffset = CGSize(width: 0, height: 4) + $0.layer.shadowOpacity = 0.1 + $0.layer.shadowColor = UIColor.black.cgColor } self.contentView.addSubview(mainView) mainView.snp.makeConstraints { @@ -82,19 +84,23 @@ final class MessageTableViewCell: UITableViewCell { $0.bottom.equalToSuperview() } - let emojiView: UIImageView = UIImageView().then { + let emojiBackgroundView: UIImageView = UIImageView().then { $0.image = UIImage(named: "emojiBox") } - mainView.addSubview(emojiView) - emojiView.snp.makeConstraints { + mainView.addSubview(emojiBackgroundView) + emojiBackgroundView.snp.makeConstraints { $0.top.leading.equalToSuperview().inset(30) $0.height.width.equalTo(40) } - + emojiImageView.do { + $0.layer.shadowOffset = CGSize(width: 0, height: 2) + $0.layer.shadowOpacity = 0.6 + $0.layer.shadowColor = UIColor.black.cgColor + } mainView.addSubview(emojiImageView) emojiImageView.snp.makeConstraints { $0.size.equalTo(20) - $0.center.equalTo(emojiView) + $0.center.equalTo(emojiBackgroundView) } self.nameLabel.do { @@ -104,15 +110,14 @@ final class MessageTableViewCell: UITableViewCell { } mainView.addSubview(self.nameLabel) self.nameLabel.snp.makeConstraints { - $0.top.equalTo(emojiView.snp.top) - $0.leading.equalTo(emojiView.snp.trailing).offset(10) + $0.top.equalTo(emojiBackgroundView.snp.top) + $0.leading.equalTo(emojiBackgroundView.snp.trailing).offset(10) $0.trailing.lessThanOrEqualToSuperview() } - mainView.addSubview(countryFlagImageView) countryFlagImageView.snp.makeConstraints { - $0.bottom.equalTo(emojiView.snp.bottom) - $0.leading.equalTo(emojiView.snp.trailing).offset(10) + $0.bottom.equalTo(emojiBackgroundView.snp.bottom) + $0.leading.equalTo(emojiBackgroundView.snp.trailing).offset(10) $0.width.height.equalTo(18) } self.countryLabel.do { @@ -138,7 +143,7 @@ final class MessageTableViewCell: UITableViewCell { mainView.addSubview(self.detailTextView) self.detailTextView.snp.makeConstraints { $0.leading.trailing.equalToSuperview().inset(30) - $0.top.equalTo(emojiView.snp.bottom).offset(10) + $0.top.equalTo(emojiBackgroundView.snp.bottom).offset(10) } self.likeView.do { @@ -206,7 +211,7 @@ final class MessageTableViewCell: UITableViewCell { likeView.play( // TODO : 각 상황에 대한 값들을 enum으로 정의할것 fromProgress: isLike ? HeartProgress.unlikeDefault.percent() : HeartProgress.likeDefault.percent(), - toProgress: isLike ? HeartProgress.likeTouchEnd.percent() : HeartProgress.unlikeTouchEnd.percent()) { (completed) in + toProgress: isLike ? HeartProgress.likeTouchEnd.percent() : HeartProgress.unlikeTouchEnd.percent()) { completed in if completed && isLike{ self?.likeCount += 1 self?.likeCountLabel.textColor = .loveRed @@ -216,65 +221,3 @@ final class MessageTableViewCell: UITableViewCell { .disposed(by: self.disposeBag) } } -//class Fdf: UIView { -// enum Progress: CGFloat { -// case likeDefault = 30 -// case unlikeDefault = 1 -// case likeTouchEnd = 15 -// case unlikeTouchEnd = 60 -// -// func percent() -> CGFloat { -// self.rawValue / 60 -// } -// } -// -// let disposeBag: DisposeBag = DisposeBag() -// let animationView: AnimationView = AnimationView(animation: Animation.named("heartfull")) -// var messageId: Int -// init(isLike: Bool, messageId: Int) { -// self.messageId = messageId -// super.init(frame: .null) -// setupUI(isLike: isLike) -// bind() -// } -// func setupUI(isLike: Bool) { -// self.addSubview(animationView) -// self.translatesAutoresizingMaskIntoConstraints = false -// self.clipsToBounds = false -// animationView.snp.makeConstraints { -// $0.top.bottom.leading.trailing.equalToSuperview() -// } -// self.animationView.currentProgress = isLike ? Progress.likeDefault.percent() : Progress.unlikeDefault.percent() -// } -// func bind() { -// self.rx.tapGesture() -// .skip(1) -// .throttle(.milliseconds(1000), latest: false, scheduler: MainScheduler.instance) -// .flatMap { [weak self] _ -> Observable in -// guard let id = self?.messageId else { -// return Observable.just(false) -// } -// return Network.request(Message.API.Like(messageId: id)) -// .map { $0?.isLiked } -// } -// .subscribe(onNext: { [weak self] isLike in -// self?.tap(isLike!) -// }) -// .disposed(by: self.disposeBag) -// } -// @objc func tap(_ isLike: Bool) { -// animationView.pause() -// if isLike { -// self.animationView.play(fromProgress: Progress.unlikeDefault.percent(), toProgress: Progress.likeTouchEnd.percent()) -// } else { -// self.animationView.play(fromProgress: Progress.likeDefault.percent(), toProgress: Progress.unlikeTouchEnd.percent()) -// } -// } -// override init(frame: CGRect) { -// self.messageId = 0 -// super.init(frame: frame) -// } -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -//} diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverReactor.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverReactor.swift index e6e1ddf..538c038 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverReactor.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverReactor.swift @@ -16,38 +16,45 @@ final class DiscoverReactor: Reactor { enum Action { case viewWillAppear case tapAbout - case countryDidChanged(country: Message.Model.Country?) - case sortTypeDidChanged(sortType: Message.Model.ListType?) + case tapFilter + case tapSort + case countryDidChanged(Model.Country?) + case sortTypeDidChanged(Model.Sort?) case refresh case loadMore } enum Mutation { - case setMessages(result: Model.Messages) + case setMessages(Model.Messages) case setRefreshing(Bool) - case addMessages(result: Model.Messages) - case setCountry(country: Model.Country?) + case addMessages(Model.Messages) + case setCountry(Model.Country?) + case setCountries([Model.Country]) case setLoading(Bool) case setPresentAboutPage(Bool) + case setPresentFilter(Bool) + case setPresentSort(Bool) case setMessageCount(Int) - case setCurrentSortType(Model.ListType) + case setCurrentSortType(Model.Sort) } struct State { - var messageCount: Int = 0 - @Revision var selectedCountry: Model.Country? - @Revision var selectedSortType: Model.ListType = .recent + @Revision var isRefreshing: Bool = false + @Revision var isPresentAboutPage: Bool = false + @Revision var isPresentFilter: Bool = false + @Revision var isPresentSort: Bool = false + @Revision var selectedCountry: Model.Country? = .wholeWorld + @Revision var countries: [Model.Country] = [] + @Revision var selectedSortType: Model.Sort = .recent @Revision var messages: Model.Messages = .init( firstMsgId: nil, lastMsgId: nil, messageCount: 0, messages: [] ) - @Revision var isRefreshing: Bool = false - + var messageCount: Int = 0 var isLoading: Bool = false var isAnimating: Bool = false - @Revision var isPresentAboutPage: Bool = false } var initialState: State @@ -77,9 +84,18 @@ final class DiscoverReactor: Reactor { ) ) .filterNil() - .map { Mutation.setMessages(result: $0) } + .map { Mutation.setMessages($0) }, + Network.request(API.Countries()) + .filterNil() + .map { .setCountries($0.countries) } ) + case .tapFilter: + return .just(.setPresentFilter(true)) + + case .tapSort: + return .just(.setPresentSort(true)) + case let .countryDidChanged(country): return .merge( Network.request( @@ -90,7 +106,7 @@ final class DiscoverReactor: Reactor { .filterNil() .map { .setMessageCount($0.messageCount) }, .concat( - .just(.setCountry(country: country)), + .just(.setCountry(country)), .just(.setLoading(true)), Network.request( API.Messages( @@ -100,7 +116,7 @@ final class DiscoverReactor: Reactor { ) ) .filterNil() - .map{ Mutation.setMessages(result: $0) }, + .map{ Mutation.setMessages($0) }, .just(.setLoading(false)) ) ) @@ -121,10 +137,11 @@ final class DiscoverReactor: Reactor { countryCode: currentState.selectedCountry?.code, lastMsgId: nil, type: sortType ?? .recent - )) - .filterNil() - .map{ .setMessages(result: $0)}, - .just(.setLoading(false)) + ) + ) + .filterNil() + .map{ Mutation.setMessages($0) }, + .just(.setLoading(false)) ) ) case .refresh: @@ -138,7 +155,7 @@ final class DiscoverReactor: Reactor { .just(.setRefreshing(true)), Network.request(Message.API.Messages(countryCode: currentState.selectedCountry?.code, lastMsgId: nil, type: currentState.selectedSortType)) .filterNil() - .map{.setMessages(result: $0)}, + .map{.setMessages($0)}, .just(.setRefreshing(false)) ])) case .loadMore: @@ -153,7 +170,7 @@ final class DiscoverReactor: Reactor { ) ) .filterNil() - .map { .addMessages(result: $0) }, + .map { .addMessages($0) }, .just(.setLoading(false)) ]) @@ -166,13 +183,13 @@ final class DiscoverReactor: Reactor { func reduce(state: State, mutation: Mutation) -> State { var newState: State = state switch mutation { - case let .setMessages(results): + case .setMessages(let results): newState.messages = results - case let .setRefreshing(flag): + case .setRefreshing(let flag): newState.isRefreshing = flag - case let .addMessages(result: results): + case .addMessages(let results): newState.messages = Model.Messages( firstMsgId: state.messages.firstMsgId, lastMsgId: results.lastMsgId, @@ -180,20 +197,29 @@ final class DiscoverReactor: Reactor { messages: currentState.messages.messages + results.messages ) - case let .setCountry(country: country): + case .setCountry(let country): newState.selectedCountry = country - case let .setLoading(flag): + case .setCountries(let countries): + newState.countries = countries + + case .setLoading(let flag): newState.isLoading = flag case .setPresentAboutPage(let isPresentAboutPage): newState.isPresentAboutPage = isPresentAboutPage - case let .setMessageCount(count): + case .setMessageCount(let count): newState.messageCount = count - case let .setCurrentSortType(type): + case .setCurrentSortType(let type): newState.selectedSortType = type + + case .setPresentFilter(let isPresentFilter): + newState.isPresentFilter = isPresentFilter + + case .setPresentSort(let isPresentSort): + newState.isPresentSort = isPresentSort } return newState } diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverViewController.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverViewController.swift index ac83d86..63f41af 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverViewController.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Discover/DiscoverViewController.swift @@ -14,10 +14,12 @@ import UIKit final class DiscoverViewController: UIViewController, View { typealias Model = Message.Model + typealias Reactor = DiscoverReactor + typealias Action = Reactor.Action // MARK: 🖼 UI private let messageCountBadgeView: MessageCountBadgeView = MessageCountBadgeView() - private let CountryFilterView: UIView = UIView() + private let filterView: UIView = UIView() private let countryLabel: UILabel = UILabel() private let messageTableView: UITableView = UITableView(frame: .null, style: .grouped) private let aboutButton: UIButton = UIButton() @@ -44,7 +46,7 @@ final class DiscoverViewController: UIViewController, View { private func startInitAnimation() { animate(view: messageCountBadgeView, alpha: 0.4, length: 20, duration: 0.4) - animate(view: CountryFilterView, alpha: 0.4, length: 20, duration: 0.4) + animate(view: filterView, alpha: 0.4, length: 20, duration: 0.4) } private func animate(view: UIView, alpha: CGFloat, length: CGFloat, duration: Double, delay: Double = 0) { @@ -80,8 +82,8 @@ final class DiscoverViewController: UIViewController, View { $0.top.greaterThanOrEqualTo(self.view.safeAreaLayoutGuide) } // 나라 필터링 뷰 - filterContainerView.addSubview(self.CountryFilterView) - self.CountryFilterView.snp.makeConstraints { + filterContainerView.addSubview(self.filterView) + self.filterView.snp.makeConstraints { $0.leading.equalToSuperview().inset(20) $0.centerY.equalToSuperview() $0.height.equalTo(20) @@ -90,21 +92,21 @@ final class DiscoverViewController: UIViewController, View { $0.font = .boldSystemFont(ofSize: 16) $0.textColor = .warmBlue } - CountryFilterView.addSubview(countryLabel) + filterView.addSubview(countryLabel) countryLabel.snp.makeConstraints { $0.centerY.equalToSuperview() - $0.leading.equalTo(CountryFilterView.snp.leading) + $0.leading.equalTo(filterView.snp.leading) $0.width.lessThanOrEqualTo(200) } let select: UIImageView = UIImageView().then { $0.image = UIImage(named: "select") } - CountryFilterView.addSubview(select) + filterView.addSubview(select) select.snp.makeConstraints { $0.centerY.equalToSuperview() $0.width.equalTo(14) $0.height.equalTo(8) - $0.trailing.equalTo(CountryFilterView.snp.trailing) + $0.trailing.equalTo(filterView.snp.trailing) $0.leading.equalTo(countryLabel.snp.trailing).offset(5) } @@ -159,10 +161,10 @@ final class DiscoverViewController: UIViewController, View { } // MARK: 🔗 Bind - func bind(reactor: DiscoverReactor) { + func bind(reactor: Reactor) { _ = AllCountries.shared reactor.action.onNext(.countryDidChanged( - country: Model.Country( + Model.Country( code: nil, fullName: "Whole World", emojiUnicode: "🍎", imageURL: nil @@ -171,9 +173,9 @@ final class DiscoverViewController: UIViewController, View { reactor.state .map(\.messageCount) - .subscribe { [weak self] count in + .subscribe(onNext: { [weak self] count in self?.messageCountBadgeView.count = count - } + }) .disposed(by: self.disposeBag) reactor.state @@ -218,58 +220,64 @@ final class DiscoverViewController: UIViewController, View { reactor.state .distinctUntilChanged(\.$selectedCountry) - .subscribe { _ in - self.messageTableView.setContentOffset(.zero, animated: false) - } + .subscribe(onNext: { [weak self] _ in + self?.messageTableView.setContentOffset(.zero, animated: false) + }) .disposed(by: self.disposeBag) reactor.state .distinctUntilChanged(\.$selectedSortType) - .subscribe { _ in - self.messageTableView.setContentOffset(.zero, animated: false) - } + .subscribe(onNext: { [weak self]_ in + self?.messageTableView.setContentOffset(.zero, animated: false) + }) + .disposed(by: self.disposeBag) + + reactor.state.distinctUntilChanged(\.$isPresentFilter) + .filter { $0.isPresentFilter } + .flatMap { [weak self] _ in self?.presentFilter() ?? .empty() } + .map { Action.countryDidChanged($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + + reactor.state.distinctUntilChanged(\.$isPresentSort) + .filter { $0.isPresentSort } + .flatMap { [weak self] _ in self?.presentSort() ?? .empty() } + .map { Action.sortTypeDidChanged($0) } + .bind(to: reactor.action) .disposed(by: self.disposeBag) self.messageTableView .refreshControl?.rx .controlEvent(.valueChanged) - .map { Reactor.Action.refresh } + .map { Action.refresh } .bind(to: reactor.action) .disposed(by: self.disposeBag) self.messageTableView .rx.isReachedBottom .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.loadMore } + .map { Action.loadMore } .bind(to: reactor.action) .disposed(by: self.disposeBag) - self.CountryFilterView + self.filterView .rx.tapGesture() .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .skip(1) - .flatMap { [weak self] _ -> Observable in - guard let self = self else { - return Observable.just(Message.Model.Country( - code: "", - fullName: "", - emojiUnicode: "", - imageURL: nil) - ) - } - return CountrySelectController.selectCountry( - presenting: self, - disposeBag: self.disposeBag, - selected: self.reactor?.currentState.selectedCountry - ) - } - .map { Reactor.Action.countryDidChanged(country: $0) } + .map { _ in Action.tapFilter } .bind(to: reactor.action) .disposed(by: self.disposeBag) + reactor.state + .distinctUntilChanged(\.$selectedSortType) + .map(\.selectedSortType) + .map(\.title) + .bind(to: self.sortLabel.rx.text) + .disposed(by: self.disposeBag) + self.aboutButton.rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) - .map { Reactor.Action.tapAbout } + .map { Action.tapAbout } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -277,15 +285,7 @@ final class DiscoverViewController: UIViewController, View { .rx.tapGesture() .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .skip(1) - .flatMap { [weak self] _ -> Observable in - guard let self = self else { return .just(Model.ListType.recent) } - return SortTypeSelectController - .select( - presenting: self, - disposeBag: self.disposeBag, - selected: self.reactor?.currentState.selectedSortType) - } - .map { Reactor.Action.sortTypeDidChanged(sortType: $0)} + .map { _ in Action.tapSort } .bind(to: reactor.action) .disposed(by: self.disposeBag) @@ -303,25 +303,58 @@ final class DiscoverViewController: UIViewController, View { .bind(to: self.refreshControl.rx.isRefreshing) .disposed(by: self.disposeBag) } + private func presentFilter() -> Observable { + guard let reactor = self.reactor else { return .empty() } + let selected: Model.Country? = reactor.currentState.selectedCountry + let items: [Model.Country] = reactor.currentState.countries + let viewController = ItemBottomSheetViewController().then { + $0.reactor = ItemBottomSheetReactor( + items: items, + selectedItem: selected, + headerItem: .wholeWorld + ) + $0.modalPresentationStyle = .overFullScreen + } + self.present(viewController, animated: true, completion: nil) + return viewController.expected.asObservable() + } + + private func presentSort() -> Observable { + guard let reactor = self.reactor else { return .empty() } + let items: [Model.Sort] = [.recent, .weeklyHot] + let viewController = ItemBottomSheetViewController().then { + $0.reactor = ItemBottomSheetReactor( + items: items, + selectedItem: reactor.currentState.selectedSortType + ) + $0.modalPresentationStyle = .overFullScreen + } + self.present(viewController, animated: false, completion: nil) + return viewController.expected.asObservable() + } } extension DiscoverViewController: UITableViewDelegate, UITableViewDataSource { - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + func tableView( + _ tableView: UITableView, + willDisplayHeaderView view: UIView, + forSection section: Int + ) { tableView.bringSubviewToFront(filterContainerView) } + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { makeHeaderView() } + private func makeHeaderView() -> UIView { let headerView: UIView = UIView() headerView.backgroundColor = .breathingWhite - // 상단 메세지 개수 표시 뷰 headerView.addSubview(self.messageCountBadgeView) messageCountBadgeView.snp.makeConstraints { $0.centerX.equalToSuperview() $0.top.equalToSuperview().inset(16) } - // 어바웃 버튼 headerView.addSubview(aboutButton) aboutButton.do { @@ -334,11 +367,15 @@ extension DiscoverViewController: UITableViewDelegate, UITableViewDataSource { } return headerView } + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { self.messages.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as? MessageTableViewCell else { return UITableViewCell() } cell.do { let cellMessage = self.messages[indexPath.row] @@ -365,7 +402,7 @@ extension DiscoverViewController: UITableViewDelegate, UITableViewDataSource { .rx.tap .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .subscribe (onNext: { - self.CountryFilterView.snp.makeConstraints { + self.filterView.snp.makeConstraints { $0.top.greaterThanOrEqualTo(self.view.safeAreaLayoutGuide) } let activityVC: UIActivityViewController = UIActivityViewController(activityItems: ["hi"], applicationActivities: nil) diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Main/Message.Model.Messages.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Main/Message.Model.Messages.swift index 73d91f0..497c67e 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Main/Message.Model.Messages.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Main/Message.Model.Messages.swift @@ -71,7 +71,7 @@ extension Message.Model { } } - enum ListType: String, CaseIterable { + enum Sort: String, CaseIterable { case recent = "recent" case weeklyHot = "weekly_hot" @@ -85,3 +85,12 @@ extension Message.Model { } } } + +extension Message.Model.Country { + static let wholeWorld: Message.Model.Country = Message.Model.Country( + code: nil, + fullName: "Whole World", + emojiUnicode: nil, + imageURL: nil + ) +} diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageReactor.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageReactor.swift index eef323b..7bfa7ee 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageReactor.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageReactor.swift @@ -19,6 +19,7 @@ final class SendMessageReactor: Reactor { case initialize case tapClose case tapRefresh + case tapFilter case typeName(String) case typeMessage(String) case tapSendMessage @@ -35,14 +36,18 @@ final class SendMessageReactor: Reactor { case setPresent(Bool) case setPresentSendAlert(Bool) case setPresentCancelAlert(Bool) + case setPresentFilter(Bool) case setCountry(Model.Country) + case setCountries([Model.Country]) } struct State: Then { @Revision var isPresented: Bool = true @Revision var isPresentSendAlert: Bool = false @Revision var isPresentCancelAlert: Bool = false - @Revision var selectedCountry: Model.Country? + @Revision var isPresentFilter: Bool = false + @Revision var selectedCountry: Model.Country = .wholeWorld + @Revision var countries: [Model.Country] = [] var emojiURL: String? var emojiIsLoading: Bool = true var canSendMessage: Bool = false @@ -78,7 +83,12 @@ final class SendMessageReactor: Reactor { func mutate(action: Action) -> Observable { switch action { case .initialize: - return .empty() + return Network.request(API.Countries()) + .filterNil() + .map { .setCountries($0.countries) } + + case .tapFilter: + return .just(.setPresentFilter(true)) case .tapClose: return .just(.setPresentCancelAlert(true)) @@ -103,11 +113,13 @@ final class SendMessageReactor: Reactor { return .just(.setPresentSendAlert(true)) case .confirmSendAlert: - guard let emojiId = currentState.emojiId else { return .empty() } + guard let emojiId = currentState.emojiId, + let countryCode = currentState.selectedCountry.code + else { return .empty() } return .concat( Network.request( API.SendMessage( - countryCode: "KR", + countryCode: countryCode, emojiId: emojiId, name: currentState.name, message: currentState.message @@ -147,7 +159,7 @@ final class SendMessageReactor: Reactor { case .setName(let name): // FIXME: 🐛 네임 텍스트필드랑 불일치 문제 수정 - let name: String = name.count <= state.nameCountLimit ? name : state.name + guard let name = name.substring(from: 0, length: state.nameCountLimit) else { return state } newState = state.with { $0.name = name $0.canSendMessage = name.isNotEmpty && $0.message.isNotEmpty @@ -158,7 +170,7 @@ final class SendMessageReactor: Reactor { } case .setMessage(let message): - let message: String = message.count <= state.messageCountLimit ? message : state.message + guard let message = message.substring(from: 0, length: state.messageCountLimit) else { return state } newState = state.with { $0.canSendMessage = $0.name.isNotEmpty && message.isNotEmpty $0.message = message @@ -183,7 +195,19 @@ final class SendMessageReactor: Reactor { } case .setCountry(let country): - newState = state.with { $0.selectedCountry = country } + newState = state.with { + $0.selectedCountry = country + } + + case .setCountries(let countries): + newState = state.with { + $0.countries = countries + } + + case .setPresentFilter(let isPresentFilter): + newState = state.with { + $0.isPresentFilter = isPresentFilter + } } return newState } @@ -214,3 +238,4 @@ final class SendMessageReactor: Reactor { + "/\(limitCount)".set(style: limitStyle) } } + diff --git a/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageViewController.swift b/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageViewController.swift index 9ff5e26..fd45a54 100644 --- a/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageViewController.swift +++ b/Dear-World/Dear-World/Source/Presentation/Scene/Send Message/SendMessageViewController.swift @@ -5,17 +5,18 @@ // Created by dongyoung.lee on 2020/12/26. // +import Kingfisher import ReactorKit import RxCocoa import RxKeyboard import RxOptional import RxSwift import UIKit -import Kingfisher import UITextView_Placeholder final class SendMessageViewController: UIViewController, View { + typealias Model = Message.Model typealias Reactor = SendMessageReactor typealias Action = Reactor.Action @@ -48,7 +49,7 @@ final class SendMessageViewController: UIViewController, View { override func viewDidLoad() { super.viewDidLoad() - setupUI() + setupUI() } // MARK: 🔗 Bind @@ -88,19 +89,12 @@ final class SendMessageViewController: UIViewController, View { selectCountryView.rx.tapGesture() .throttle(.milliseconds(300), scheduler: MainScheduler.instance) .skip(1) - .flatMap { [weak self] _ -> Observable in - guard let self = self else { return .empty() } - return CountrySelectController.selectCountry( - presenting: self, - disposeBag: self.disposeBag, - selected: nil) - } - .map { Action.countryDidChange($0) } + .map { _ in Action.tapFilter } .bind(to: reactor.action) .disposed(by: disposeBag) reactor.state.distinctUntilChanged(\.$selectedCountry) - .map { $0.selectedCountry?.fullName } + .map { $0.selectedCountry.fullName } .bind(to: selectCountryView.titleLabel.rx.text) .disposed(by: disposeBag) @@ -115,6 +109,14 @@ final class SendMessageViewController: UIViewController, View { }) .disposed(by: disposeBag) + reactor.state + .distinctUntilChanged(\.$isPresentFilter) + .filter { $0.isPresentFilter } + .flatMap { [weak self] _ in self?.presentFilter() ?? .empty() } + .map { Action.countryDidChange($0) } + .bind(to: reactor.action) + .disposed(by: self.disposeBag) + reactor.state.map(\.emojiURL) .map { URL(string: $0) } .filterNil() @@ -130,6 +132,7 @@ final class SendMessageViewController: UIViewController, View { .disposed(by: disposeBag) reactor.state.map(\.name) + .distinctUntilChanged() .filter { [weak self] name in self?.nameTextField.text != name } @@ -143,6 +146,7 @@ final class SendMessageViewController: UIViewController, View { reactor.state .map(\.message) + .distinctUntilChanged() .filter { [weak self] message in self?.messageTextView.text != message } @@ -265,7 +269,11 @@ final class SendMessageViewController: UIViewController, View { $0.top.equalTo(selectCountryView.snp.bottom).offset(20) $0.width.height.equalTo(80) } - + emojiImageView.do { + $0.layer.shadowOffset = CGSize(width: 0, height: 10) + $0.layer.shadowOpacity = 0.6 + $0.layer.shadowColor = UIColor.black.cgColor + } self.view.addSubview(emojiImageView) emojiImageView.snp.makeConstraints { $0.center.equalTo(profileBackgroundView) @@ -436,4 +444,20 @@ final class SendMessageViewController: UIViewController, View { self?.rotateArrowImageViews() } } + + private func presentFilter() -> Observable { + guard let reactor = self.reactor else { return .empty() } + let selected: Model.Country? = reactor.currentState.selectedCountry + let items: [Model.Country] = reactor.currentState.countries + let viewController = ItemBottomSheetViewController().then { + $0.reactor = ItemBottomSheetReactor( + items: items, + selectedItem: selected, + headerItem: .wholeWorld + ) + $0.modalPresentationStyle = .overFullScreen + } + self.present(viewController, animated: true, completion: nil) + return viewController.expected.asObservable() + } } diff --git a/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItem.swift b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItem.swift new file mode 100644 index 0000000..3ccaf09 --- /dev/null +++ b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItem.swift @@ -0,0 +1,24 @@ +// +// BottomSheetItem.swift +// Dear-World +// +// Created by dongyoung.lee on 2021/01/12. +// + +import Foundation + +protocol BottomSheetItem { + var name: String { get } + var imageURL: String? { get } +} +extension BottomSheetItem { + var imageURL: String? { nil } +} + +extension Message.Model.Country: BottomSheetItem { + var name: String { fullName } +} + +extension Message.Model.Sort: BottomSheetItem { + var name: String { title } +} diff --git a/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItemCell.swift b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItemCell.swift new file mode 100644 index 0000000..d1e4d84 --- /dev/null +++ b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItemCell.swift @@ -0,0 +1,61 @@ +// +// BottomSheetItemCell.swift +// Dear-World +// +// Created by dongyoung.lee on 2021/01/11. +// + +import UIKit + +final class BottomSheetItemCell: UITableViewCell { + + private let checkImageView: UIImageView = UIImageView() + + override var isSelected: Bool { + didSet { + if isSelected { + textLabel?.textColor = .livelyBlue + contentView.backgroundColor = .breathingWhite + } else { + textLabel?.textColor = .warmBlue + contentView.backgroundColor = .white + } + checkImageView.isHidden = !isSelected + } + } + + // MARK: 🏁 Initialize + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: 🎛 Setup + private func setupUI() { + self.do { + $0.backgroundColor = .white + $0.textLabel?.textColor = .warmBlue + $0.textLabel?.font = .systemFont(ofSize: 14) + } + checkImageView.do { + $0.isHidden = !isSelected + $0.image = UIImage(named: "check") + } + contentView.addSubview(checkImageView) + checkImageView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().inset(20) + $0.width.equalTo(12) + $0.height.equalTo(10) + } + } + + // MARK: 🔩 Configuration + func configure(item: BottomSheetItem) { + textLabel?.text = item.name + } +} diff --git a/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItemHeaderView.swift b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItemHeaderView.swift new file mode 100644 index 0000000..16992a5 --- /dev/null +++ b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/BottomSheetItemHeaderView.swift @@ -0,0 +1,87 @@ +// +// BottomSheetItemHeaderView.swift +// Dear-World +// +// Created by dongyoung.lee on 2021/01/11. +// + +import ReactorKit +import RxCocoa +import RxSwift +import UIKit + +final class BottomSheetItemHeaderView: UIView { + + // MARK: 🖼 UI + private let checkImageView: UIImageView = UIImageView() + private let nameLabel: UILabel = UILabel() + + let headerItem: PublishRelay = PublishRelay() + var isSelected: Bool = false { + didSet { + if isSelected { + nameLabel.textColor = .livelyBlue + backgroundColor = .breathingWhite + } else { + nameLabel.textColor = .warmBlue + backgroundColor = .white + } + checkImageView.isHidden = !isSelected + } + } + private let disposeBag: DisposeBag = DisposeBag() + + // MARK: 🏁 Initialize + init() { + super.init(frame: .zero) + + setupUI() + bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: 🎛 Setup + private func setupUI() { + self.do { + $0.backgroundColor = .white + } + + nameLabel.do { + $0.textColor = .warmBlue + $0.font = .boldSystemFont(ofSize: 15) + } + self.addSubview(nameLabel) + nameLabel.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.leading.equalToSuperview().inset(20) + } + + checkImageView.do { + $0.isHidden = !isSelected + $0.image = UIImage(named: "check") + } + self.addSubview(checkImageView) + checkImageView.snp.makeConstraints { + $0.centerY.equalToSuperview() + $0.trailing.equalToSuperview().inset(20) + $0.width.equalTo(12) + $0.height.equalTo(10) + } + } + + // MARK: 🔗 Bind + private func bind() { + headerItem.map { $0.name } + .distinctUntilChanged() + .bind(to: nameLabel.rx.text) + .disposed(by: disposeBag) + + headerItem.map { $0.name.isEmpty } + .distinctUntilChanged() + .bind(to: self.rx.isHidden) + .disposed(by: disposeBag) + } +} diff --git a/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/ItemBottomReactor.swift b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/ItemBottomReactor.swift new file mode 100644 index 0000000..4463dd4 --- /dev/null +++ b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/ItemBottomReactor.swift @@ -0,0 +1,80 @@ +// +// SelectItemReactor.swift +// Dear-World +// +// Created by dongyoung.lee on 2021/01/11. +// + +import Foundation +import ReactorKit + +final class ItemBottomSheetReactor: Reactor { + + enum Action { + case tapClose + case tapBackground + case tapHeader + case selectItem(at: Int) + } + + enum Mutation { + case setPresent(Bool) + case setSelectedItem(Item) + } + + struct State { + @Revision var isPresent: Bool + @Revision var headerItem: Item? + @Revision var selectedItem: Item? + @Revision var items: [Item] + } + + let initialState: State + + // MARK: 🏁 Initialize + init( + items: [Item], + selectedItem: Item? = nil, + headerItem: Item? = nil + ) { + initialState = State( + isPresent: true, + headerItem: headerItem, + selectedItem: selectedItem, + items: items + ) + } + + // MARK: 🔫 Mutate + func mutate(action: Action) -> Observable { + switch action { + case .tapHeader: + guard let headerItem = currentState.headerItem else { return .empty() } + return .just(.setSelectedItem(headerItem)) + + case .tapClose: + return .just(.setPresent(false)) + + case .tapBackground: + return .just(.setPresent(false)) + + case .selectItem(let index): + let item: Item = currentState.items[index] + return .from([.setSelectedItem(item), + .setPresent(false)]) + } + } + + // MARK: ⚡️ Reduce + func reduce(state: State, mutation: Mutation) -> State { + var newState: State = state + switch mutation { + case .setPresent(let isPresent): + newState.isPresent = isPresent + + case .setSelectedItem(let item): + newState.selectedItem = item + } + return newState + } +} diff --git a/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/ItemBottomSheetViewController.swift b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/ItemBottomSheetViewController.swift new file mode 100644 index 0000000..7192176 --- /dev/null +++ b/Dear-World/Dear-World/Source/Presentation/View/BottomSheet/ItemBottomSheetViewController.swift @@ -0,0 +1,167 @@ +// +// SelectImageBottomSheet.swiwt +// Dear-World +// +// Created by dongyoung.lee on 2021/01/11. +// + +import ReactorKit +import RxCocoa +import RxSwift +import UIKit + +final class ItemBottomSheetViewController: UIViewController, Promisable, View { + + typealias Expected = Item + typealias Reactor = ItemBottomSheetReactor + typealias Action = Reactor.Action + + // MARK: 🖼 UI + let bottomSheet: UIStackView = UIStackView() + private let closeButton: UIButton = UIButton() + private let bottomSheetHeaderView: BottomSheetItemHeaderView = BottomSheetItemHeaderView() + private let itemsTableView: UITableView = UITableView() + private var selectedItem: Item? + var disposeBag: DisposeBag = DisposeBag() + + // MARK: 🏁 Initialize + init() { + super.init(nibName: nil, bundle: nil) + + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: 🎛 Setup + private func setupUI() { + self.view.do { + $0.backgroundColor = UIColor.black.withAlphaComponent(0.6) + } + bottomSheet.do { + $0.axis = .vertical + } + self.view.addSubview(bottomSheet) + bottomSheet.snp.makeConstraints { + $0.leading.trailing.bottom.equalToSuperview() + } + + let headerBar: UIView = UIView().then { + $0.layer.cornerRadius = 15 + $0.layer.maskedCorners = [.layerMinXMinYCorner, + .layerMaxXMinYCorner] + $0.backgroundColor = .white + } + bottomSheet.addArrangedSubview(headerBar) + headerBar.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(52) + } + + closeButton.do { + $0.setImage(UIImage(named: "cancel"), for: .normal) + $0.imageEdgeInsets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + } + headerBar.addSubview(closeButton) + closeButton.snp.makeConstraints { + $0.size.equalTo(52) + $0.top.trailing.equalToSuperview() + } + + bottomSheet.addArrangedSubview(bottomSheetHeaderView) + bottomSheetHeaderView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(56) + } + bottomSheet.addArrangedSubview(itemsTableView) + + itemsTableView.do { + $0.rowHeight = 56 + $0.separatorStyle = .none + $0.register(BottomSheetItemCell.self, forCellReuseIdentifier: "BottomSheetItemCell") + } + itemsTableView.snp.makeConstraints { + $0.top.equalTo(bottomSheetHeaderView.snp.bottom) + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(442) + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + reactor?.action.onNext(.tapBackground) + } + + // MARK: 🔗 Bind + func bind(reactor: Reactor) { + + bottomSheetHeaderView.rx.tapGesture() + .skip(1) + .map { _ in Action.tapHeader } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + closeButton.rx.tap + .map { Action.tapClose } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + itemsTableView.rx.itemSelected + .map { Action.selectItem(at: $0.row) } + .bind(to: reactor.action) + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.$items) + .map { $0.items } + .bind(to: itemsTableView.rx.items( + cellIdentifier: "BottomSheetItemCell", + cellType: BottomSheetItemCell.self + )) { [weak self] _, item, cell in + cell.isSelected = item.name == self?.selectedItem?.name + cell.configure(item: item) + } + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.$headerItem) + .map { $0.headerItem } + .filterNil() + .bind(to: bottomSheetHeaderView.headerItem) + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.$headerItem) + .map { $0.headerItem } + .map { $0 == nil } + .bind(to: bottomSheetHeaderView.rx.isHidden) + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.$selectedItem) + .map { $0.selectedItem } + .do { [weak self] in + self?.selectedItem = $0 + self?.bottomSheetHeaderView.isSelected = $0?.name == self?.reactor?.currentState.headerItem?.name + } + .filterNil() + .bind(to: expected) + .disposed(by: disposeBag) + + reactor.state.distinctUntilChanged(\.$isPresent) + .filter { !$0.isPresent } + .subscribe(onNext: { [weak self] _ in + self?.close() + }) + .disposed(by: disposeBag) + } + + private func close() { + UIView.animate(withDuration: 0.3) { + self.bottomSheet.snp.makeConstraints { + $0.top.equalTo(self.view.snp.bottom) + } + self.view.layoutIfNeeded() + } completion: { [weak self] _ in + self?.dismiss(animated: false, completion: nil) + } + } +} + diff --git a/Dear-World/Dear-World/Source/Presentation/View/CountrySelectController.swift b/Dear-World/Dear-World/Source/Presentation/View/CountrySelectController.swift index 39b5164..ac85d4c 100644 --- a/Dear-World/Dear-World/Source/Presentation/View/CountrySelectController.swift +++ b/Dear-World/Dear-World/Source/Presentation/View/CountrySelectController.swift @@ -11,8 +11,8 @@ import SnapKit import Then import UIKit -public final class CountrySelectController: UIViewController { - +final class CountrySelectController: UIViewController { + typealias Model = Message.Model // MARK: 🖼 UI private var countryTableView: UITableView = UITableView() private let countries: [Message.Model.Country] = AllCountries.shared.countries @@ -20,9 +20,10 @@ public final class CountrySelectController: UIViewController { private var selectedCountry: Message.Model.Country? = nil private let wholeWorldButton: UIButton = UIButton() private let outsideView: UIView = UIView() + let disposeBag: DisposeBag = DisposeBag() - override public func viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() setupUI() setupTableView() @@ -66,7 +67,7 @@ public final class CountrySelectController: UIViewController { } } insideView.addSubview(wholeWorldButton) - let wholeWorldLabel = UILabel().then { + let wholeWorldLabel: UILabel = UILabel().then { $0.text = "Whole world" $0.font = .systemFont(ofSize: 14) if selectedCountry?.code == nil { @@ -116,8 +117,8 @@ public final class CountrySelectController: UIViewController { .disposed(by: self.disposeBag) Observable.just(self.countries) - .bind(to: self.countryTableView.rx.items) { [weak self] (tableView, row, item) -> UITableViewCell in - guard let self = self else { return UITableViewCell()} + .bind(to: self.countryTableView.rx.items) { [weak self] tableView, row, item -> UITableViewCell in + guard let self = self else { return UITableViewCell() } let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "countryCell", for: IndexPath(row: row, section: 0)) cell.textLabel?.text = item.fullName cell.textLabel?.textColor = .warmBlue @@ -132,7 +133,7 @@ public final class CountrySelectController: UIViewController { cell.subviews.last?.isHidden = false } cell.textLabel?.font = .systemFont(ofSize: 14) - let cellBackgroudView = UIView() + let cellBackgroudView: UIView = UIView() cellBackgroudView.backgroundColor = .breathingWhite cell.selectedBackgroundView = cellBackgroudView cell.textLabel?.snp.makeConstraints { @@ -158,13 +159,14 @@ public final class CountrySelectController: UIViewController { self.countryTableView .rx.itemSelected - .map{[weak self] in self?.countries[$0.row]} - .subscribe (onNext :{ [weak self] country in + .map { [weak self] in self?.countries[$0.row] } + .subscribe(onNext: { [weak self] country in self?.selectedCountry = country self?.willMove(toParent: nil) }) .disposed(by: self.disposeBag) } + func addCheck(base: UIView) { let checkImage: UIImageView = UIImageView().then { $0.image = UIImage(named: "check") @@ -180,6 +182,7 @@ public final class CountrySelectController: UIViewController { } // present 함수 extension CountrySelectController { + static func selectCountry( presenting: UIViewController, disposeBag: DisposeBag, diff --git a/Dear-World/Dear-World/Source/Presentation/View/NoticeBadge.swift b/Dear-World/Dear-World/Source/Presentation/View/NoticeBadge.swift index 0d2e388..d15b029 100644 --- a/Dear-World/Dear-World/Source/Presentation/View/NoticeBadge.swift +++ b/Dear-World/Dear-World/Source/Presentation/View/NoticeBadge.swift @@ -28,7 +28,7 @@ final class NoticeBadge: UILabel { } override var intrinsicContentSize: CGSize { - let size = super.intrinsicContentSize + let size: CGSize = super.intrinsicContentSize return CGSize(width: size.width + contentInsets.left + contentInsets.right, height: size.height + contentInsets.top + contentInsets.bottom) } diff --git a/Dear-World/Dear-World/Source/Presentation/View/SortTypeSelectController.swift b/Dear-World/Dear-World/Source/Presentation/View/SortTypeSelectController.swift index b745386..b652be3 100644 --- a/Dear-World/Dear-World/Source/Presentation/View/SortTypeSelectController.swift +++ b/Dear-World/Dear-World/Source/Presentation/View/SortTypeSelectController.swift @@ -5,7 +5,6 @@ // Created by rookie.w on 2021/01/07. // - import RxCocoa import RxSwift import SnapKit @@ -13,11 +12,11 @@ import Then import UIKit public final class SortTypeSelectController: UIViewController { - private var sortTypes: [Message.Model.ListType] = Message.Model.ListType.allCases + private var sortTypes: [Message.Model.Sort] = Message.Model.Sort.allCases // MARK: 🖼 UI private var sortTypeTableView: UITableView = UITableView() private let exitButton: UIButton = UIButton() - private var selectedSortType: Message.Model.ListType? = nil + private var selectedSortType: Message.Model.Sort? private let outsideView: UIView = UIView() let disposeBag: DisposeBag = DisposeBag() @@ -89,8 +88,8 @@ public final class SortTypeSelectController: UIViewController { .disposed(by: self.disposeBag) Observable.just(self.sortTypes) - .bind(to: self.sortTypeTableView.rx.items) { [weak self] (tableView, row, item) -> UITableViewCell in - guard let self = self else { return UITableViewCell()} + .bind(to: self.sortTypeTableView.rx.items) { [weak self] tableView, row, item -> UITableViewCell in + guard let self = self else { return UITableViewCell() } let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "sortTypeCell", for: IndexPath(row: row, section: 0)) cell.textLabel?.text = item.title cell.textLabel?.textColor = .warmBlue @@ -105,7 +104,7 @@ public final class SortTypeSelectController: UIViewController { cell.subviews.last?.isHidden = false } cell.textLabel?.font = .systemFont(ofSize: 14) - let cellBackgroudView = UIView() + let cellBackgroudView: UIView = UIView() cellBackgroudView.backgroundColor = .breathingWhite cell.selectedBackgroundView = cellBackgroudView return cell @@ -114,7 +113,7 @@ public final class SortTypeSelectController: UIViewController { self.sortTypeTableView .rx.itemSelected - .map{[weak self] in self?.sortTypes[$0.row]} + .map { [weak self] in self?.sortTypes[$0.row] } .bind { [weak self] sortType in self?.selectedSortType = sortType self?.willMove(toParent: nil) @@ -137,8 +136,12 @@ public final class SortTypeSelectController: UIViewController { // present 함수 extension SortTypeSelectController { - static func select(presenting: UIViewController, disposeBag: DisposeBag, selected: Message.Model.ListType?) -> Observable { - return Observable.create { observer in + static func select( + presenting: UIViewController, + disposeBag: DisposeBag, + selected: Message.Model.Sort? + ) -> Observable { + return Observable.create { observer in guard let base = presenting.tabBarController else { observer.onError(NSError()) return Disposables.create() @@ -159,7 +162,7 @@ extension SortTypeSelectController { presented.rx.methodInvoked(#selector(UIViewController.willMove(toParent:))) .bind { _ in - if let type: Message.Model.ListType = presented.selectedSortType { + if let type: Message.Model.Sort = presented.selectedSortType { observer.onNext(type) } UIView.animate(withDuration: 0.3) { diff --git a/Dear-World/Dear-World/Source/Presentation/View/TextView.swift b/Dear-World/Dear-World/Source/Presentation/View/TextView.swift index 622ab9e..509e016 100644 --- a/Dear-World/Dear-World/Source/Presentation/View/TextView.swift +++ b/Dear-World/Dear-World/Source/Presentation/View/TextView.swift @@ -22,21 +22,21 @@ open class TextView: UITextView { public let placeholderLabel: UILabel = UILabel() - private var placeholderLabelConstraints = [NSLayoutConstraint]() + private var placeholderLabelConstraints: [NSLayoutConstraint] = [NSLayoutConstraint]() - @IBInspectable open var placeholder: String = "" { + @IBInspectable var placeholder: String = "" { didSet { placeholderLabel.text = placeholder } } - @IBInspectable open var placeholderColor: UIColor = Constants.defaultiOSPlaceholderColor { + @IBInspectable var placeholderColor: UIColor = Constants.defaultiOSPlaceholderColor { didSet { placeholderLabel.textColor = placeholderColor } } - override open var font: UIFont! { + open override var font: UIFont! { didSet { if placeholderFont == nil { placeholderLabel.font = font @@ -44,32 +44,32 @@ open class TextView: UITextView { } } - open var placeholderFont: UIFont? { + var placeholderFont: UIFont? { didSet { let font = (placeholderFont != nil) ? placeholderFont : self.font placeholderLabel.font = font } } - override open var textAlignment: NSTextAlignment { + open override var textAlignment: NSTextAlignment { didSet { placeholderLabel.textAlignment = textAlignment } } - override open var text: String! { + open override var text: String! { didSet { textDidChange() } } - override open var attributedText: NSAttributedString! { + open override var attributedText: NSAttributedString! { didSet { textDidChange() } } - override open var textContainerInset: UIEdgeInsets { + open override var textContainerInset: UIEdgeInsets { didSet { updateConstraintsForPlaceholderLabel() } @@ -87,9 +87,9 @@ open class TextView: UITextView { private func commonInit() { #if swift(>=4.2) - let notificationName = UITextView.textDidChangeNotification + let notificationName: NSNotification.Name = UITextView.textDidChangeNotification #else - let notificationName = NSNotification.Name.UITextView.textDidChangeNotification + let notificationName: NSNotification.Name = NSNotification.Name.UITextView.textDidChangeNotification #endif NotificationCenter.default.addObserver( @@ -111,7 +111,7 @@ open class TextView: UITextView { } private func updateConstraintsForPlaceholderLabel() { - var newConstraints = NSLayoutConstraint.constraints( + var newConstraints: [NSLayoutConstraint] = NSLayoutConstraint.constraints( withVisualFormat: "H:|-(\(textContainerInset.left + textContainer.lineFragmentPadding))-[placeholder]", options: [], metrics: nil, @@ -146,25 +146,26 @@ open class TextView: UITextView { placeholderLabelConstraints = newConstraints } - @objc private func textDidChange() { + @objc + private func textDidChange() { placeholderLabel.isHidden = !text.isEmpty } - open override func layoutSubviews() { + open + override func layoutSubviews() { super.layoutSubviews() placeholderLabel.preferredMaxLayoutWidth = textContainer.size.width - textContainer.lineFragmentPadding * 2.0 } deinit { #if swift(>=4.2) - let notificationName = UITextView.textDidChangeNotification + let notificationName: NSNotification.Name = UITextView.textDidChangeNotification #else - let notificationName = NSNotification.Name.UITextView.textDidChangeNotification + let notificationName: NSNotification.Name = NSNotification.Name.UITextView.textDidChangeNotification #endif NotificationCenter.default.removeObserver(self, name: notificationName, object: nil) } - }