Skip to content

Commit

Permalink
Fullscreen Image (#1499)
Browse files Browse the repository at this point in the history
  • Loading branch information
EricBAndrews authored Dec 16, 2024
1 parent 929083d commit d85e6f5
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 21 deletions.
6 changes: 5 additions & 1 deletion Mlem.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
CD09BA7F2CB4698E00C93926 /* OledPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD09BA7E2CB4698600C93926 /* OledPalette.swift */; };
CD0E06F72C0E739F00445849 /* PostType+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0E06F62C0E739F00445849 /* PostType+Extensions.swift */; };
CD0F280A2C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */; };
CD10CC182D0A0A650006C20F /* MediaState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD10CC172D0A0A5C0006C20F /* MediaState.swift */; };
CD10FA772C7A8622008985AD /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD10FA762C7A8622008985AD /* ImageSaver.swift */; };
CD13CC572C582DD8001AF428 /* DynamicMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC562C582DD8001AF428 /* DynamicMediaView.swift */; };
CD13CC592C583C7A001AF428 /* WebsitePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */; };
Expand Down Expand Up @@ -707,6 +708,7 @@
CD0E06F62C0E739F00445849 /* PostType+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostType+Extensions.swift"; sourceTree = "<group>"; };
CD0E07002C12707700445849 /* ToolbarEllipsisMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarEllipsisMenu.swift; sourceTree = "<group>"; };
CD0F28092C6CEFBE00C1F65B /* View+IsAtTopSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+IsAtTopSubscriber.swift"; sourceTree = "<group>"; };
CD10CC172D0A0A5C0006C20F /* MediaState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaState.swift; sourceTree = "<group>"; };
CD10FA762C7A8622008985AD /* ImageSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = "<group>"; };
CD13CC562C582DD8001AF428 /* DynamicMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicMediaView.swift; sourceTree = "<group>"; };
CD13CC582C583C7A001AF428 /* WebsitePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1666,13 +1668,14 @@
CD4D59212B87BD0800B82964 /* Definitions */ = {
isa = PBXGroup;
children = (
CDCA44AE2C17672900C092B3 /* HapticManager */,
CD4D58B22B86BFD400B82964 /* AccountsTracker.swift */,
036CC3AE2B8145C30098B6A1 /* AppState.swift */,
0380965E2C10AA80003ED1D8 /* AppState+Transition.swift */,
CD10CC172D0A0A5C0006C20F /* MediaState.swift */,
CD317D4B2BE97FED008F63E2 /* Palette.swift */,
CD4D58A42B86BD1B00B82964 /* PersistenceRepository.swift */,
CD4ED8462BF110FA00EFA0A2 /* TabReselectTracker.swift */,
CDCA44AE2C17672900C092B3 /* HapticManager */,
);
path = Definitions;
sourceTree = "<group>";
Expand Down Expand Up @@ -2338,6 +2341,7 @@
CD7DB9762C4D6C0A00DCC542 /* FeedCommentView.swift in Sources */,
03D3A1EF2BB9CA1D009DE55E /* MenuButton.swift in Sources */,
CD0507732C6AA0C8008B1505 /* FeedSelection.swift in Sources */,
CD10CC182D0A0A650006C20F /* MediaState.swift in Sources */,
039F58862C7A810100C61658 /* ExpandedPostView+Logic.swift in Sources */,
031E2D512BEF961D0003BC45 /* SubscriptionListView.swift in Sources */,
CDD4A09E2C8B69FC0001AD1A /* Button.swift in Sources */,
Expand Down
13 changes: 13 additions & 0 deletions Mlem/App/Globals/Definitions/MediaState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// MediaState.swift
// Mlem
//
// Created by Eric Andrews on 2024-12-11.
//

import SwiftUI

@Observable
class MediaState {
var url: URL?
}
93 changes: 87 additions & 6 deletions Mlem/App/Views/Pages/ImageViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,106 @@ import SwiftUI

struct ImageViewer: View {
@Environment(Palette.self) var palette
@Environment(MediaState.self) var mediaState

let url: URL

let duration: CGFloat = 0.25
let screenHeight: CGFloat = UIScreen.main.bounds.height

@GestureState var dragState: Bool = false

@State var isZoomed: Bool = false
@State var offset: CGFloat = 0
@State var isDismissing: Bool = false
@State var opacity: CGFloat = 0

init(url: URL) {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
components.queryItems = components.queryItems?.filter { $0.name != "thumbnail" }
self.url = components.url!
}

var body: some View {
ZoomableContainer {
DynamicMediaView(url: url, playImmediately: true)
ZoomableContainer(isZoomed: $isZoomed) {
DynamicMediaView(url: url, cornerRadius: 0, playImmediately: true)
}
.offset(y: offset)
.background(.black)
.opacity(opacity)
.overlay(alignment: .topTrailing) {
if offset == 0 {
Button {
fadeDismiss()
} label: {
Image(systemName: Icons.close)
.resizable()
.frame(width: 15, height: 15)
.foregroundStyle(.white)
.padding([.top, .trailing], Constants.main.standardSpacing)
.padding([.bottom, .leading], Constants.main.doubleSpacing)
.contentShape(.rect)
}
.padding(Constants.main.standardSpacing)
}
}
.simultaneousGesture(DragGesture(minimumDistance: 0.0)
.updating($dragState) { value, state, _ in
state = true
if !isZoomed, !isDismissing {
offset = value.translation.height
opacity = 1.0 - (abs(value.translation.height) / screenHeight)
}
}
)
.onAppear {
updateOpacity(1.0)
}
.onChange(of: dragState) {
if !dragState {
if abs(offset) > 100 {
swipeDismiss(finalOffset: offset > 0 ? screenHeight : -screenHeight)
} else {
updateDragDistance(0)
}
}
}
}

private func fadeDismiss() {
isDismissing = true
updateOpacity(0) {
mediaState.url = nil
}
}

private func swipeDismiss(finalOffset: CGFloat = UIScreen.main.bounds.height) {
isDismissing = true
updateDragDistance(finalOffset) {
mediaState.url = nil
}
}

private func updateOpacity(_ newOpacity: CGFloat, callback: (() -> Void)? = nil) {
withAnimation(.easeOut(duration: duration)) {
opacity = newOpacity
}
if let callback {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
callback()
}
}
}

private func updateDragDistance(_ newDistance: CGFloat, callback: (() -> Void)? = nil) {
withAnimation(.easeOut(duration: duration)) {
offset = newDistance
opacity = 1.0 - (abs(newDistance) / screenHeight)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
CloseButtonView()
if let callback {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
callback()
}
}
.background(palette.background)
}
}
11 changes: 11 additions & 0 deletions Mlem/App/Views/Root/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct ContentView: View {
var palette: Palette { .main }
var tabReselectTracker: TabReselectTracker { .main }
var navigationModel: NavigationModel { .main }
var mediaState: MediaState = .init()

@State var avatarImage: UIImage?
@State var selectedAvatarImage: UIImage?
Expand Down Expand Up @@ -102,6 +103,7 @@ struct ContentView: View {
}
}
}
.environment(mediaState)
.environment(AppState.main)
}
}
Expand Down Expand Up @@ -170,5 +172,14 @@ struct ContentView: View {
location: .top
)
}
.overlay {
if let url = mediaState.url {
NavigationLayerView(
layer: .init(root: .imageViewer(url),
model: navigationModel,
hasNavigationStack: false),
hasSheetModifiers: false)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private struct AnimationControlLayer: ViewModifier {
}
}
}
.overlay(alignment: .topTrailing) {
.overlay(alignment: .bottomTrailing) {
if let muted {
Image(systemName: muted.wrappedValue ? Icons.muted : Icons.unmuted)
.resizable()
Expand Down
9 changes: 3 additions & 6 deletions Mlem/App/Views/Shared/Images/Wrappers/LargeImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import SwiftUI
// and anything else that can't go in thumbnail views etc.

struct LargeImageView: View {
@Environment(MediaState.self) var mediaState

@Environment(NavigationLayer.self) private var navigation
@Setting(\.blurNsfw) var blurNsfw

Expand Down Expand Up @@ -55,12 +57,7 @@ struct LargeImageView: View {
onTapActions()
}
if let loading, loading == .done, let url {
// Sheets don't cover the whole screen on iPad, so use a fullScreenCover instead
if UIDevice.isPad {
navigation.showFullScreenCover(.imageViewer(url))
} else {
navigation.openSheet(.imageViewer(url))
}
mediaState.url = url
}
}
.onPreferenceChange(MediaLoadingPreferenceKey.self, perform: { loading = $0 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SwiftUI
struct ThumbnailImageView: View {
@Environment(Palette.self) var palette
@Environment(NavigationLayer.self) var navigation
@Environment(MediaState.self) var mediaState
@Environment(\.openURL) var openURL

@State var loading: MediaLoadingState?
Expand Down Expand Up @@ -55,12 +56,7 @@ struct ThumbnailImageView: View {
if let loading, loading == .done || loading == .proxyFailed {
post.markRead()

// Sheets don't cover the whole screen on iPad, so use a fullScreenCover instead
if UIDevice.isPad {
navigation.showFullScreenCover(.imageViewer(url))
} else {
navigation.openSheet(.imageViewer(url))
}
mediaState.url = url
}
}
.contextMenu {
Expand Down
2 changes: 2 additions & 0 deletions Mlem/App/Views/Shared/Navigation/NavigationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class NavigationModel {
var showingFilePicker: Bool = false
var filePickerCallback: ((URL) -> Void)?

var mediaUrl: URL?

private func openSheet(_ page: NavigationPage, hasNavigationStack: Bool? = nil, isFullScreenCover: Bool) {
layers.append(
.init(
Expand Down
7 changes: 6 additions & 1 deletion Mlem/App/Views/Shared/ZoomableContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ struct ZoomableContainer<Content: View>: View {
let content: Content
@State private var currentScale: CGFloat = 1.0
@State private var tapLocation: CGPoint = .zero
@Binding var isZoomed: Bool

init(@ViewBuilder content: () -> Content) {
init(isZoomed: Binding<Bool> = .constant(false), @ViewBuilder content: () -> Content) {
self.content = content()
self._isZoomed = isZoomed
}

func doubleTapAction(location: CGPoint) {
Expand All @@ -32,6 +34,9 @@ struct ZoomableContainer<Content: View>: View {
content
}
.onTapGesture(count: 2, perform: doubleTapAction)
.onChange(of: currentScale) {
isZoomed = currentScale != 1.0
}
}

fileprivate struct ZoomableScrollView<ScollContent: View>: UIViewRepresentable {
Expand Down

0 comments on commit d85e6f5

Please sign in to comment.