|
| 1 | +import SwiftUI |
| 2 | + |
| 3 | +class ReaderSiteHeaderView: UIView, ReaderStreamHeader { |
| 4 | + |
| 5 | + weak var delegate: ReaderStreamHeaderDelegate? |
| 6 | + |
| 7 | + private lazy var headerViewModel: ReaderSiteHeaderViewModel = { |
| 8 | + ReaderSiteHeaderViewModel(onFollowTap: { [weak self] completion in |
| 9 | + guard let self else { |
| 10 | + return |
| 11 | + } |
| 12 | + self.delegate?.handleFollowActionForHeader(self, completion: completion) |
| 13 | + }) |
| 14 | + }() |
| 15 | + |
| 16 | + init() { |
| 17 | + super.init(frame: .zero) |
| 18 | + backgroundColor = .secondarySystemGroupedBackground |
| 19 | + setupHeader() |
| 20 | + } |
| 21 | + |
| 22 | + required init?(coder: NSCoder) { |
| 23 | + fatalError("init(coder:) has not been implemented") |
| 24 | + } |
| 25 | + |
| 26 | + func enableLoggedInFeatures(_ enable: Bool) { |
| 27 | + headerViewModel.isFollowHidden = !enable |
| 28 | + } |
| 29 | + |
| 30 | + func configureHeader(_ topic: ReaderAbstractTopic) { |
| 31 | + guard let siteTopic = topic as? ReaderSiteTopic else { |
| 32 | + assertionFailure("This header should only be used for site topics.") |
| 33 | + return |
| 34 | + } |
| 35 | + headerViewModel.imageUrl = siteTopic.siteBlavatar |
| 36 | + headerViewModel.title = siteTopic.title |
| 37 | + headerViewModel.siteUrl = URL(string: siteTopic.siteURL)?.host ?? "" |
| 38 | + headerViewModel.siteDetails = siteTopic.siteDescription |
| 39 | + headerViewModel.postCount = siteTopic.postCount.doubleValue.abbreviatedString() |
| 40 | + headerViewModel.followerCount = siteTopic.subscriberCount.doubleValue.abbreviatedString() |
| 41 | + headerViewModel.isFollowingSite = siteTopic.following |
| 42 | + } |
| 43 | + |
| 44 | + private func setupHeader() { |
| 45 | + weak var weakSelf = self |
| 46 | + let header = ReaderSiteHeader(viewModel: weakSelf?.headerViewModel ?? ReaderSiteHeaderViewModel()) |
| 47 | + let view = UIView.embedSwiftUIView(header) |
| 48 | + addSubview(view) |
| 49 | + pinSubviewToAllEdges(view) |
| 50 | + } |
| 51 | + |
| 52 | +} |
| 53 | + |
| 54 | +// MARK: - ReaderSiteHeader |
| 55 | + |
| 56 | +struct ReaderSiteHeader: View { |
| 57 | + |
| 58 | + @StateObject var viewModel: ReaderSiteHeaderViewModel |
| 59 | + |
| 60 | + var body: some View { |
| 61 | + VStack(alignment: .leading, spacing: 12) { |
| 62 | + AsyncImage(url: URL(string: viewModel.imageUrl)) { phase in |
| 63 | + switch phase { |
| 64 | + case .success(let image): |
| 65 | + image |
| 66 | + .resizable() |
| 67 | + .frame(width: 72.0, height: 72.0) |
| 68 | + .clipShape(Circle()) |
| 69 | + default: |
| 70 | + Image(Constants.defaultSiteImage) |
| 71 | + .resizable() |
| 72 | + .frame(width: 72.0, height: 72.0) |
| 73 | + .clipShape(Circle()) |
| 74 | + } |
| 75 | + } |
| 76 | + VStack(alignment: .leading, spacing: 4) { |
| 77 | + Text(viewModel.title) |
| 78 | + .font(Font(WPStyleGuide.serifFontForTextStyle(.title2, fontWeight: .semibold))) |
| 79 | + Text(viewModel.siteUrl) |
| 80 | + .font(.subheadline) |
| 81 | + } |
| 82 | + if !viewModel.siteDetails.isEmpty { |
| 83 | + Text(viewModel.siteDetails) |
| 84 | + .lineLimit(3) |
| 85 | + .font(.subheadline) |
| 86 | + .foregroundColor(.secondary) |
| 87 | + } |
| 88 | + countsDisplay |
| 89 | + if !viewModel.isFollowHidden { |
| 90 | + followButton |
| 91 | + .padding(.top, 4) |
| 92 | + } |
| 93 | + } |
| 94 | + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) |
| 95 | + .padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16)) |
| 96 | + .background(Color(UIColor.listForeground)) |
| 97 | + } |
| 98 | + |
| 99 | + private var countsDisplay: some View { |
| 100 | + let countsString = String(format: Constants.countsFormat, viewModel.postCount, viewModel.followerCount) |
| 101 | + let stringItems = countsString.components(separatedBy: " ") |
| 102 | + |
| 103 | + return stringItems.reduce(Text(""), { |
| 104 | + var text = Text($1) |
| 105 | + if $1 == viewModel.postCount || $1 == viewModel.followerCount { |
| 106 | + text = text.font(.subheadline) |
| 107 | + } else { |
| 108 | + text = text.font(.subheadline).foregroundColor(.secondary) |
| 109 | + } |
| 110 | + return $0 + text + Text(" ") |
| 111 | + }) |
| 112 | + } |
| 113 | + |
| 114 | + @ViewBuilder |
| 115 | + private var followButton: some View { |
| 116 | + if viewModel.isFollowingSite { |
| 117 | + Button { |
| 118 | + viewModel.updateFollowStatus() |
| 119 | + } label: { |
| 120 | + Image(uiImage: Constants.followingIcon ?? UIImage()) |
| 121 | + .padding(.leading, -2.0) |
| 122 | + .padding(.trailing, 2.0) |
| 123 | + Text(WPStyleGuide.FollowButton.Text.followingStringForDisplay) |
| 124 | + .padding(.leading, 2.0) |
| 125 | + .padding(.trailing, -2.0) |
| 126 | + .foregroundColor(.secondary) |
| 127 | + .font(.callout) |
| 128 | + } |
| 129 | + .disabled(!viewModel.isFollowEnabled) |
| 130 | + .padding(.horizontal, 12.0) |
| 131 | + .padding(.vertical, 6.0) |
| 132 | + .overlay( |
| 133 | + RoundedRectangle(cornerRadius: 4) |
| 134 | + .stroke(Color(UIColor.primaryButtonBorder), lineWidth: 1) |
| 135 | + ) |
| 136 | + } else { |
| 137 | + Button { |
| 138 | + viewModel.updateFollowStatus() |
| 139 | + } label: { |
| 140 | + Image(uiImage: Constants.followIcon ?? UIImage()) |
| 141 | + .padding(.leading, -2.0) |
| 142 | + .padding(.trailing, 2.0) |
| 143 | + Text(WPStyleGuide.FollowButton.Text.followStringForDisplay) |
| 144 | + .padding(.leading, 2.0) |
| 145 | + .padding(.trailing, -2.0) |
| 146 | + .foregroundColor(.white) |
| 147 | + .font(.callout.weight(.semibold)) |
| 148 | + } |
| 149 | + .disabled(!viewModel.isFollowEnabled) |
| 150 | + .padding(.horizontal, 12.0) |
| 151 | + .padding(.vertical, 6.0) |
| 152 | + .background(Color(UIColor.primary)) |
| 153 | + .cornerRadius(4) |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + struct Constants { |
| 158 | + static let defaultSiteImage = "blavatar-default" |
| 159 | + static let iconSide = WPStyleGuide.fontSizeForTextStyle(.callout) |
| 160 | + static let followIconSize = CGSize(width: iconSide, height: iconSide) |
| 161 | + static let followIcon = UIImage.gridicon(.readerFollow, size: followIconSize).imageWithTintColor(.white) |
| 162 | + static let followingIcon = UIImage.gridicon(.readerFollowing, size: followIconSize).imageWithTintColor(.buttonIcon) |
| 163 | + static let countsFormat = NSLocalizedString("reader.site.header.counts", |
| 164 | + value: "%1$@ posts • %2$@ followers", |
| 165 | + comment: "The formatted number of posts and followers for a site. " + |
| 166 | + "'%1$@' is a placeholder for the site post count. " + |
| 167 | + "'%2$@' is a placeholder for the site follower count. " + |
| 168 | + "Example: `5,000 posts • 10M followers`") |
| 169 | + } |
| 170 | + |
| 171 | +} |
| 172 | + |
| 173 | +// MARK: - ReaderSiteHeaderViewModel |
| 174 | + |
| 175 | +class ReaderSiteHeaderViewModel: ObservableObject { |
| 176 | + |
| 177 | + @Published var imageUrl: String |
| 178 | + @Published var title: String |
| 179 | + @Published var siteUrl: String |
| 180 | + @Published var siteDetails: String |
| 181 | + @Published var postCount: String |
| 182 | + @Published var followerCount: String |
| 183 | + @Published var isFollowingSite: Bool |
| 184 | + @Published var isFollowHidden: Bool |
| 185 | + @Published var isFollowEnabled: Bool |
| 186 | + |
| 187 | + private let onFollowTap: (_ completion: @escaping () -> Void) -> Void |
| 188 | + |
| 189 | + init(imageUrl: String = "", |
| 190 | + title: String = "", |
| 191 | + siteUrl: String = "", |
| 192 | + siteDetails: String = "", |
| 193 | + postCount: String = "", |
| 194 | + followerCount: String = "", |
| 195 | + isFollowingSite: Bool = false, |
| 196 | + isFollowHidden: Bool = false, |
| 197 | + isFollowEnabled: Bool = true, |
| 198 | + onFollowTap: @escaping (_ completion: @escaping () -> Void) -> Void = { _ in }) { |
| 199 | + self.imageUrl = imageUrl |
| 200 | + self.title = title |
| 201 | + self.siteUrl = siteUrl |
| 202 | + self.siteDetails = siteDetails |
| 203 | + self.postCount = postCount |
| 204 | + self.followerCount = followerCount |
| 205 | + self.isFollowingSite = isFollowingSite |
| 206 | + self.isFollowHidden = isFollowHidden |
| 207 | + self.isFollowEnabled = isFollowEnabled |
| 208 | + self.onFollowTap = onFollowTap |
| 209 | + } |
| 210 | + |
| 211 | + func updateFollowStatus() { |
| 212 | + isFollowEnabled = false |
| 213 | + onFollowTap { [weak self] in |
| 214 | + self?.isFollowEnabled = true |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | +} |
0 commit comments