Fix unclickable elements

The introduction of iOS 18 brought a new bug that made `KFAnimatedImage`
not recognize tap gestures and become unclickable. (https://github.com/onevcat/Kingfisher/issues/2295)

This commit addresses the issue with a workaround found here:
https://github.com/onevcat/Kingfisher/issues/2046#issuecomment-1554068070

The workaround was suggested by the author of the library to fix a
slightly different issue, but that property seems to work for our
purposes.

The issue is addressed by adding a `contentShape` property to usages
of `KFAnimatedImage`, in order to make them clickable. A custom modifier
was created to make the solution less obscure and more obvious.

Furthermore, one empty tap gesture handler was removed as it was
preventing other tap gesture handlers on the image carousel from being
triggered on iOS 18

Testing
-------

PASS

Configurations:
- iPhone 13 mini on iOS 18.0
- iPhone SE simulator on iOS 17.5
Damus: This commit
Coverage:
- Check that the following views are clickable:
    - Images in the carousel
    - Profile picture on notes
    - Profile picture on thread comments
    - Profile picture on profile page

Changelog-Fixed: Fix items that became unclickable on iOS 18
Closes: https://github.com/damus-io/damus/issues/2342
Closes: https://github.com/damus-io/damus/issues/2370
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2024-09-20 13:10:54 -07:00
parent 6254cea600
commit 823c2565da
13 changed files with 57 additions and 2 deletions

View File

@@ -1111,6 +1111,8 @@
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; }; D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; };
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; }; D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; }; D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; }; D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
@@ -1959,6 +1961,7 @@
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; }; D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; }; D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; }; D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; }; D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; }; D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; }; D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; };
@@ -2540,6 +2543,7 @@
4C75EFA227FA576C0006080F /* Views */ = { 4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D7D68FF72C9E01A80015A515 /* Utils */,
D78DB85D2C20FE9E00F0AB12 /* Chat */, D78DB85D2C20FE9E00F0AB12 /* Chat */,
D71AC4CA2BA8E3320076268E /* Extensions */, D71AC4CA2BA8E3320076268E /* Extensions */,
BA3759952ABCCF360018D73B /* Camera */, BA3759952ABCCF360018D73B /* Camera */,
@@ -3368,6 +3372,14 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D7D68FF72C9E01A80015A515 /* Utils */ = {
isa = PBXGroup;
children = (
D7D68FF82C9E01B60015A515 /* KFClickable.swift */,
);
path = Utils;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = { E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -3803,6 +3815,7 @@
B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */, B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */,
4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */, 4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */,
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
D74AAFCF2B155D8C006CF0F4 /* ZapDataModel.swift in Sources */, D74AAFCF2B155D8C006CF0F4 /* ZapDataModel.swift in Sources */,
@@ -4517,6 +4530,7 @@
D73E5F332C6A97F4007EB227 /* ZapEvent.swift in Sources */, D73E5F332C6A97F4007EB227 /* ZapEvent.swift in Sources */,
D73E5F342C6A97F4007EB227 /* TextEvent.swift in Sources */, D73E5F342C6A97F4007EB227 /* TextEvent.swift in Sources */,
D73E5F352C6A97F4007EB227 /* WideEventView.swift in Sources */, D73E5F352C6A97F4007EB227 /* WideEventView.swift in Sources */,
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */,
D73E5F8A2C6AA69C007EB227 /* SideMenuView.swift in Sources */, D73E5F8A2C6AA69C007EB227 /* SideMenuView.swift in Sources */,
D73E5F362C6A97F4007EB227 /* LongformView.swift in Sources */, D73E5F362C6A97F4007EB227 /* LongformView.swift in Sources */,
D73E5F372C6A97F4007EB227 /* LongformPreview.swift in Sources */, D73E5F372C6A97F4007EB227 /* LongformPreview.swift in Sources */,

View File

@@ -236,6 +236,7 @@ struct ImageCarousel<Content: View>: View {
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count) Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
} }
.aspectRatio(contentMode: filling ? .fill : .fit) .aspectRatio(contentMode: filling ? .fill : .fit)
.kfClickable()
.position(x: geo.size.width / 2, y: geo.size.height / 2) .position(x: geo.size.width / 2, y: geo.size.height / 2)
.tabItem { .tabItem {
Text(url.absoluteString) Text(url.absoluteString)
@@ -274,8 +275,14 @@ struct ImageCarousel<Content: View>: View {
var body: some View { var body: some View {
VStack { VStack {
Medias if #available(iOS 18.0, *) {
.onTapGesture { } Medias
} else {
// An empty tap gesture recognizer is needed on iOS 17 and below to suppress other overlapping tap recognizers
// Otherwise it will both open the carousel and go to a note at the same time
Medias.onTapGesture { }
}
if urls.count > 1 { if urls.count > 1 {
PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count) PageControlView(currentPage: $model.selectedIndex, numberOfPages: urls.count)

View File

@@ -29,6 +29,7 @@ struct EditBannerImageView: View {
Color(uiColor: .secondarySystemBackground) Color(uiColor: .secondarySystemBackground)
} }
.onFailureImage(defaultImage) .onFailureImage(defaultImage)
.kfClickable()
EditPictureControl(uploader: damus_state.settings.default_media_uploader, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback) EditPictureControl(uploader: damus_state.settings.default_media_uploader, pubkey: damus_state.pubkey, image_url: $banner_image, uploadObserver: viewModel, callback: callback)
} }
@@ -54,6 +55,7 @@ struct InnerBannerImageView: View {
Color(uiColor: .secondarySystemBackground) Color(uiColor: .secondarySystemBackground)
} }
.onFailureImage(defaultImage) .onFailureImage(defaultImage)
.kfClickable()
} else { } else {
Image(uiImage: defaultImage).resizable() Image(uiImage: defaultImage).resizable()
} }

View File

@@ -50,6 +50,7 @@ struct HighlightEventRef: View {
FailedImage() FailedImage()
} }
.frame(width: 35, height: 35) .frame(width: 35, height: 35)
.kfClickable()
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
.scaledToFit() .scaledToFit()

View File

@@ -61,6 +61,7 @@ struct HighlightLink: View {
.background(DamusColors.adaptableWhite) .background(DamusColors.adaptableWhite)
} }
.frame(width: 35, height: 35) .frame(width: 35, height: 35)
.kfClickable()
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
.scaledToFit() .scaledToFit()
} else { } else {

View File

@@ -98,6 +98,7 @@ struct LongformPreviewBody: View {
} }
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150) .frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
.kfClickable()
.cornerRadius(1) .cornerRadius(1)
} }

View File

@@ -33,6 +33,7 @@ struct ImageContainerView: View {
view.framePreloadCount = 3 view.framePreloadCount = 3
} }
.imageModifier(ImageHandler(handler: $image)) .imageModifier(ImageHandler(handler: $image))
.kfClickable()
.clipped() .clipped()
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet)) .modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) { .sheet(isPresented: $showShareSheet) {

View File

@@ -33,6 +33,7 @@ struct ProfileImageContainerView: View {
.imageModifier(ImageHandler(handler: $image)) .imageModifier(ImageHandler(handler: $image))
.clipShape(Circle()) .clipShape(Circle())
.modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet)) .modifier(ImageContextMenuModifier(url: url, image: image, settings: settings, showShareSheet: $showShareSheet))
.kfClickable()
.sheet(isPresented: $showShareSheet) { .sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url]) ShareSheet(activityItems: [url])
} }

View File

@@ -71,6 +71,7 @@ struct EditPictureControl: View {
} }
.scaledToFill() .scaledToFill()
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10) .frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
.kfClickable()
.foregroundColor(DamusColors.white) .foregroundColor(DamusColors.white)
.clipShape(Circle()) .clipShape(Circle())
.overlay(Circle().stroke(.white, lineWidth: 4)) .overlay(Circle().stroke(.white, lineWidth: 4))

View File

@@ -57,6 +57,7 @@ struct InnerProfilePicView: View {
} }
.scaledToFill() .scaledToFill()
.frame(width: size, height: size) .frame(width: size, height: size)
.kfClickable()
.clipShape(Circle()) .clipShape(Circle())
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight))) .overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
} }

View File

@@ -31,6 +31,7 @@ struct EditProfilePictureView: View {
view.framePreloadCount = 3 view.framePreloadCount = 3
} }
.scaledToFill() .scaledToFill()
.kfClickable()
EditPictureControl(uploader: damus_state?.settings.default_media_uploader ?? .nostrBuild, pubkey: pubkey, image_url: $profile_url, uploadObserver: uploadObserver, callback: callback) EditPictureControl(uploader: damus_state?.settings.default_media_uploader ?? .nostrBuild, pubkey: pubkey, image_url: $profile_url, uploadObserver: uploadObserver, callback: callback)
} }

View File

@@ -55,6 +55,7 @@ struct InnerRelayPicView: View {
Placeholder(url: url) Placeholder(url: url)
} }
.scaledToFit() .scaledToFit()
.kfClickable()
} else { } else {
FailedRelayImage(url: nil) FailedRelayImage(url: nil)
} }

View File

@@ -0,0 +1,23 @@
//
// ClickableOverlay.swift
// damus
//
// Created by Daniel DAquino on 2024-09-20.
//
import SwiftUI
/// Applies a property that makes `KFAnimatedImage` clickable again on iOS 18+
fileprivate struct KFClickable: ViewModifier {
func body(content: Content) -> some View {
content
.contentShape(Rectangle())
}
}
extension View {
/// Applies a property that makes `KFAnimatedImage` clickable again on iOS 18+
func kfClickable() -> some View {
return self.modifier(KFClickable())
}
}