Skip to content

Commit bfb0256

Browse files
Update avatar button for Liquid Glass
Use the intended API for configuring the tab by setting a simple image rather than manipulating the view hierarchy of the tab bar item. The image is now just a cicular crop of the avatar, with no border and no up-down chevron to indicate account switching, although the account switcher still works. Contributes to IOS-624
1 parent c29be99 commit bfb0256

File tree

1 file changed

+108
-37
lines changed

1 file changed

+108
-37
lines changed

Mastodon/Scene/Root/MainTab/MainTabBarController.swift

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import MastodonAsset
1313
import MastodonCore
1414
import MastodonLocalization
1515
import MastodonUI
16+
import SDWebImage
1617

1718
class MainTabBarController: UITabBarController {
1819

@@ -218,11 +219,15 @@ extension MainTabBarController {
218219
.receive(on: DispatchQueue.main)
219220
.sink { [weak self] avatarURL in
220221
guard let self else { return }
221-
self.avatarButton.avatarImageView.setImage(
222-
url: avatarURL,
223-
placeholder: .placeholder(color: .systemFill),
224-
scaleToSize: MainTabBarController.avatarButtonSize
225-
)
222+
if #available(iOS 26, *) {
223+
self.layoutAvatarButton()
224+
} else {
225+
self.avatarButton.avatarImageView.setImage(
226+
url: avatarURL,
227+
placeholder: .placeholder(color: .systemFill),
228+
scaleToSize: MainTabBarController.avatarButtonSize
229+
)
230+
}
226231
}
227232
.store(in: &disposeBag)
228233

@@ -379,39 +384,50 @@ extension MainTabBarController {
379384
}
380385

381386
private func layoutAvatarButton() {
382-
guard avatarButton.superview == nil else { return }
383-
384-
guard let profileTabItem = meProfileViewController.tabBarItem else { return }
385-
guard let view = profileTabItem.value(forKey: "view") as? UIView else {
386-
return
387-
}
388-
389-
let _anchorImageView = view.subviews.first { subview in subview is UIImageView } as? UIImageView
390-
guard let anchorImageView = _anchorImageView else {
391-
assertionFailure()
392-
return
387+
if #available(iOS 26, *) {
388+
guard let avatarURL else { return }
389+
Task { [weak self] in
390+
guard let self else { return }
391+
let avatarImage = try await AvatarButtonImageLoader.getImage(url: avatarURL)
392+
guard self.avatarURL == avatarURL else { return }
393+
let tabBarItem = UITabBarItem(title: nil, image: avatarImage?.withRenderingMode(.alwaysOriginal), tag: Tab.me.tag)
394+
self.meProfileViewController.tabBarItem = tabBarItem
395+
}
396+
} else {
397+
guard avatarButton.superview == nil else { return }
398+
399+
guard let profileTabItem = meProfileViewController.tabBarItem else { return }
400+
guard let view = profileTabItem.value(forKey: "view") as? UIView else {
401+
return
402+
}
403+
404+
let _anchorImageView = view.subviews.first { subview in subview is UIImageView } as? UIImageView
405+
guard let anchorImageView = _anchorImageView else {
406+
assertionFailure()
407+
return
408+
}
409+
anchorImageView.alpha = 0
410+
411+
accountSwitcherChevron.removeFromSuperview()
412+
accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false
413+
view.addSubview(accountSwitcherChevron)
414+
415+
self.avatarButton.translatesAutoresizingMaskIntoConstraints = false
416+
view.addSubview(self.avatarButton)
417+
NSLayoutConstraint.activate([
418+
self.avatarButton.centerXAnchor.constraint(equalTo: anchorImageView.centerXAnchor),
419+
self.avatarButton.centerYAnchor.constraint(equalTo: anchorImageView.centerYAnchor),
420+
self.avatarButton.widthAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.width).priority(.required - 1),
421+
self.avatarButton.heightAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.height).priority(.required - 1),
422+
accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 10),
423+
accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 18),
424+
accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8),
425+
accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor)
426+
])
427+
self.avatarButton.setContentHuggingPriority(.required - 1, for: .horizontal)
428+
self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
429+
self.avatarButton.isUserInteractionEnabled = false
393430
}
394-
anchorImageView.alpha = 0
395-
396-
accountSwitcherChevron.removeFromSuperview()
397-
accountSwitcherChevron.translatesAutoresizingMaskIntoConstraints = false
398-
view.addSubview(accountSwitcherChevron)
399-
400-
self.avatarButton.translatesAutoresizingMaskIntoConstraints = false
401-
view.addSubview(self.avatarButton)
402-
NSLayoutConstraint.activate([
403-
self.avatarButton.centerXAnchor.constraint(equalTo: anchorImageView.centerXAnchor),
404-
self.avatarButton.centerYAnchor.constraint(equalTo: anchorImageView.centerYAnchor),
405-
self.avatarButton.widthAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.width).priority(.required - 1),
406-
self.avatarButton.heightAnchor.constraint(equalToConstant: MainTabBarController.avatarButtonSize.height).priority(.required - 1),
407-
accountSwitcherChevron.widthAnchor.constraint(equalToConstant: 10),
408-
accountSwitcherChevron.heightAnchor.constraint(equalToConstant: 18),
409-
accountSwitcherChevron.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 8),
410-
accountSwitcherChevron.centerYAnchor.constraint(equalTo: avatarButton.centerYAnchor)
411-
])
412-
self.avatarButton.setContentHuggingPriority(.required - 1, for: .horizontal)
413-
self.avatarButton.setContentHuggingPriority(.required - 1, for: .vertical)
414-
self.avatarButton.isUserInteractionEnabled = false
415431
}
416432

417433
private func updateAvatarButtonAppearance() {
@@ -690,3 +706,58 @@ extension MainTabBarController: UINavigationControllerDelegate {
690706
}
691707
}
692708
}
709+
710+
struct AvatarButtonImageLoader {
711+
static var _cache = [URL : UIImage]()
712+
713+
static func getImage(url: URL) async throws -> UIImage? {
714+
if let cached = _cache[url] {
715+
return cached
716+
} else {
717+
let baseImage: UIImage? = try await withCheckedThrowingContinuation { continuation in
718+
SDWebImageDownloader.shared.downloadImage(with: url) { image, data, error, finished in
719+
if let error {
720+
continuation.resume(throwing: error)
721+
} else {
722+
continuation.resume(returning: image)
723+
}
724+
}
725+
}
726+
727+
guard let circular = baseImage?.circularMasked(diameter: 40) else { return nil }
728+
_cache[url] = circular
729+
return circular
730+
}
731+
}
732+
733+
}
734+
735+
var circularAvatarRenderer: UIGraphicsImageRenderer?
736+
737+
extension UIImage {
738+
func circularMasked(diameter: CGFloat) -> UIImage? {
739+
guard let cgImage = self.cgImage else { return nil }
740+
let scale = max(diameter/size.width, diameter/size.height)
741+
let drawingRect = CGRect(
742+
x: (size.width * scale - diameter) / 2,
743+
y: (size.height * scale - diameter) / 2,
744+
width: size.width * scale,
745+
height: size.height * scale
746+
)
747+
748+
let renderer = {
749+
if let circularAvatarRenderer {
750+
return circularAvatarRenderer
751+
} else {
752+
let _newRenderer = UIGraphicsImageRenderer(size: CGSize(width: diameter, height: diameter))
753+
circularAvatarRenderer = _newRenderer
754+
return _newRenderer
755+
}
756+
}()
757+
return renderer.image { context in
758+
let circleRect = CGRect(origin: .zero, size: CGSize(width: diameter, height: diameter))
759+
UIBezierPath(ovalIn: circleRect).addClip()
760+
UIImage(cgImage: cgImage).draw(in: drawingRect)
761+
}
762+
}
763+
}

0 commit comments

Comments
 (0)