Giphy / giphy-ios-sdk

Home of the GIPHY SDK iOS example app, along with iOS SDK documentation, issue tracking, & release notes.
https://developers.giphy.com/
Mozilla Public License 2.0
111 stars 49 forks source link

SwiftUI Example #44

Open AlexClarkQP opened 4 years ago

AlexClarkQP commented 4 years ago

Do you have any documentation for SwiftUI integration? Specifically I’d like to show the GiphyViewController in sheet but it’s showing a sheet within a sheet which looks a little awkward.

Here's the code I have so far:

import GiphyUISDK
import GiphyCoreSDK
import SwiftUI

struct GiphyPicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<GiphyPicker>) -> GiphyViewController {
        GiphyViewController()
    }

    func updateUIViewController(_ uiViewController: GiphyViewController, context: UIViewControllerRepresentableContext<GiphyPicker>) {
    }
}

and in my view:

.sheet(isPresented: showGiphy) {
     GiphyPicker()
}
cgmaier commented 4 years ago

hey @AlexClarkQP i'm actually not sure ! have not yet explored it

https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit you can't find the answer here?

AlexClarkQP commented 4 years ago

The interfacing with UIKit is fine. I think it's more an issue with how GiphyViewController presents itself. It appears to present itself in a tray over an existing view (rather than just being the entire view). So if you present it over your entire app it looks fine. However, if you present it in a sheet (the new preferred modal style in iOS 13) it looks odd. I've attached a screenshot. I was wondering if there's a setting to have GiphyViewController just show itself in a full screen view rather than in a tray. I've tried trayHeightMultiplier but it doesn't fill the screen.

Original

ealeksandrov commented 3 years ago

GiphyViewController is still useless in SwiftUI. I've used GiphyGridController as a fallback, but it's quite limited in comparison.

For example, SCSDKBitmojiStickerPickerViewController from Snapchat has similar controls (search + stickers), but doesn't hijack whole screen and works well as UIViewControllerRepresentable inside SwiftUI.

Simple GiphyGridController as UIViewControllerRepresentable implementation (no search, just trending GIFs):

struct GiphySheet: UIViewControllerRepresentable {
    typealias UIViewControllerType = GiphyGridController

    let pickedImageCallback: (String) -> Void

    func makeUIViewController(context: Context) -> GiphyGridController {
        let giphyVC = GiphyGridController()
        giphyVC.delegate = context.coordinator
        giphyVC.content = GPHContent.trending(mediaType: .gif)
        giphyVC.update()
        return giphyVC
    }
    func updateUIViewController(_ uiViewController: GiphyGridController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, GPHGridDelegate {
        var parent: GiphySheet

        init(_ giphySheet: GiphySheet) {
            self.parent = giphySheet
        }

        func contentDidUpdate(resultCount: Int) {
            //
        }

        func didSelectMedia(media: GPHMedia, cell: UICollectionViewCell) {
            parent.pickedImageCallback(media.url)
        }
    }
}
schazers commented 3 years ago

+1 on this! Am using SwiftUI, and this would be very helpful 👍

schazers commented 3 years ago

Just a note to @ealeksandrov and anyone who finds the info useful: contrary to the above, GiphyViewController is useful in SwiftUI. Here's how to get it working. I am wrapping it inside a UIVIewControllerRepresentable and presenting it atop a live camera view by using a ZStack. I'm placing the view atop a semi-transparent black background, and providing the top and bottom of the view with some padding. To make GiphyViewController enter the screen by filling the full height of its tray, use the static method call GiphyViewController.trayHeightMultiplier = 1.0 when initializing it (see below). This line prevents the issue shown above where it appears to fill just some of its tray's height. @AlexClarkQP - I think the last line was your issue.

It would be nice to have some extra SwiftUI support without having to wrap inside a UIViewControllerRepresentable, of course, but it's not much work to do it ourselves. It would also be nice, Giphy team, to have SwiftUI-friendly ways to render the returned media (GiphyYYImage). It's still a very useful controller to drop in, though. Thanks for providing it.

Here's a minimal example which accomplishes the above. The end of it contains an excerpt from a SwiftUI view which uses the UIViewControllerRepresentable. I haven't made the latter portion clean or re-usable yet, but it should get the idea across:

import Foundation
import SwiftUI
import UIKit
import GiphyUISDK

struct GiphyVCRepresentable : UIViewControllerRepresentable {
    public typealias UIViewControllerType = GiphyViewController

    // gif, width, height
    var onSelectedGif: (GiphyYYImage, CGFloat, CGFloat) -> Void
    var onShouldDismissGifPicker: () -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    public func makeUIViewController(context: UIViewControllerRepresentableContext<GiphyVCRepresentable>) -> GiphyViewController {
        let gvc = GiphyViewController()
        gvc.delegate = context.coordinator
        //gvc.mediaTypeConfig = [] // TODO: can filter by media type
        gvc.theme = CustomGiphyTheme()
        GiphyViewController.trayHeightMultiplier = 1.0 // This causes the tray to start at the screen's full height
        return gvc
    }

    public func updateUIViewController(_ giphyViewController: GiphyViewController, context: UIViewControllerRepresentableContext<GiphyVCRepresentable>) {
        // TODO:
    }

    class Coordinator: NSObject, GiphyDelegate {
        let parent: GiphyVCRepresentable
        init(_ parent: GiphyVCRepresentable) {
            self.parent = parent
        }

        func didSearch(for term: String) {
            print("GiphyDelegate: the user made searched for: ", term)
        }

        func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {
            giphyViewController.dismiss(animated: true, completion: { [weak self] in
                let url = media.url(rendition: giphyViewController.renditionType, fileType: .webp) ?? ""
                GPHCache.shared.downloadAsset(url) { (image, error) in
                    DispatchQueue.main.async {
                        //let imageView = GiphyYYAnimatedImageView()
                        let width = CGFloat(media.images?.fixedWidth?.width ?? 0)
                        let height = CGFloat(media.images?.fixedWidth?.height ?? 0)
                        if let _giphyYYImage = image, let _coordinator = self {
                            _coordinator.parent.onSelectedGif(_giphyYYImage, width, height)
                        }
                    }
                }
            })
        }

        func didDismiss(controller: GiphyViewController?) {
            self.parent.onShouldDismissGifPicker()
        }
    }
}

public class CustomGiphyTheme: GPHTheme {
    public override init() {
        super.init()
        self.type = .darkBlur
    }

    public override var textFieldFont: UIFont? {
        return UIFont(name: "Futura-Medium", size: 15)
    }

    public override var textColor: UIColor {
        return .white
    }
}

// Here's a usage example excerpted from the middle of a SwiftUI View, where the ZStack below is placed inside of another ZStack from that containing view, and sits on top of the other view's content in that ZStack.

// ... inside some SwiftUI View ...

@State private var isShowingGifPicker = false

// ... somewhere inside the body of that SwiftUI VIew ...

if self.isShowingGifPicker {
ZStack {
    VStack(spacing: 0) {
         // Most of the screen's background becomes a partially transparent
         // black view. Tapping anywhere on it dismisses the Giphy view.
         Rectangle()
             .fill(Color.init(white: 0.0, opacity: 0.5))
             .onTapGesture {
              withAnimation {
                  self.isShowingGifPicker = false
              }

         // This little bit covers the bottom area between the GiphyVC and
         // the remaining sliver near the screen's bottom safe area
         Rectangle()
             .fill(Color.init(white: (38.0/255.0)))
             .frame(height: 100)
     }

     GiphyVCRepresentable(onSelectedGif: { _giphyYYImage, width, height in
         // TODO: do something with _giphyYYImage for your app
         withAnimation {
             self.isShowingGifPicker = false
         }
      }, onShouldDismissGifPicker: {
         withAnimation {
             self.isShowingGifPicker = false
          }
      })
      .padding(.bottom, 20.0)
      .padding(.top, 90.0)
      .transition(.move(edge: .bottom))
}
}
ealeksandrov commented 3 years ago

Thank you for sharing your solution @schazers! This is a viable option for full-screen popover (but I haven't fully tested it).

Unfortunately it won't fit some use cases, like showing GIFs picker below the input field, in place of keyboard:

comparison

As you can see - SCSDKBitmojiStickerPickerViewController works fine, because it doesn't expect to handle whole screen interaction, it just works anywhere you place it. GiphyGridController example also works fine, but lacks some features like search.

puelocesar commented 3 years ago

IMO, the best way of doing it is using Introspect:

.introspectViewController { controller in
                guard presentingGiphy else { return }
                let giphyController = GiphyViewController()
                giphyController.delegate = myViewModel
                controller.present(giphyController, animated: true, completion: nil)
            }

Code doesn't look great, but at least this way GiphyViewController works properly

Patresko commented 1 year ago

Still no better support for swiftui in 2022? Solutions from @schazers cause memory leak and 100% CPU usage.

// 2022 solution.

I wrapped GiphyViewController inside custom UIViewControllerRepresentable

First create GiphyView

struct GifPicker: UIViewControllerRepresentable {

    @State var theme: GPHThemeType = .dark

    var completion: ((String) -> Void)
    var onShouldDismissGifPicker: () -> Void

    func makeUIViewController(context: Context) -> GiphyViewController {
        Giphy.configure(apiKey: "jdD6Mf0lensylI78b48ce701AkxfMglw")

        let controller = GiphyViewController()
        controller.swiftUIEnabled = true
        controller.mediaTypeConfig = [.gifs, .stickers, .recents]
        controller.delegate = context.coordinator
        controller.navigationController?.isNavigationBarHidden = true
        controller.navigationController?.setNavigationBarHidden(true, animated: false)

        GiphyViewController.trayHeightMultiplier = 1.0

        controller.theme = GPHTheme(type: theme)

        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {

    }

    func makeCoordinator() -> Coordinator {
        return  GifPicker.Coordinator(parent: self)
    }

    class Coordinator: NSObject, GiphyDelegate {

        var parent: GifPicker

        init(parent: GifPicker) {
            self.parent = parent
        }

        func didDismiss(controller: GiphyViewController?) {
            self.parent.onShouldDismissGifPicker()
        }

        func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {

            // retrieving url
            let url = media.url(rendition: .fixedWidth, fileType: .gif)
            DispatchQueue.main.async {
                self.parent.completion(url ?? "")
            }
        }
    }
}

Now create our custom halfSheet

private struct GifSheet<SheetView: View>: UIViewControllerRepresentable{

    var sheetView: SheetView

    @Binding var isPresented: Bool

    let controller = UIViewController()

    func makeUIViewController(context: Context) -> some UIViewController {

        controller.view.backgroundColor = .clear

        return controller
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        if isPresented {

            let sheetController = GifCustomHostingController(rootView: sheetView)
            sheetController.view.backgroundColor = .clear

            uiViewController.present(sheetController, animated: true) {
                DispatchQueue.main.async {
                    self.isPresented.toggle()
                }
            }
        }
    }
}

class GifCustomHostingController<Content: View>: UIHostingController<Content> {

    override func viewDidLoad() {
        if let presentationController = presentationController as? UISheetPresentationController {
            presentationController.detents = [
                .medium(),
                .large()
            ]

            presentationController.prefersGrabberVisible = false
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.view.invalidateIntrinsicContentSize()
        }
}

extension View {
    func gifSheet<SheetView: View>(isPresented: Binding<Bool>, @ViewBuilder sheetView: @escaping ()-> SheetView )-> some View {
        return self
            .background(
                GifSheet(sheetView: sheetView(), isPresented: isPresented)
            )
    }
}

Usage in SwiftUI

.gifSheet(isPresented: $chatLogViewModel.isGifPickerVisible) {
            GifPicker(theme: colorScheme == .light ? .lightBlur : .darkBlur) { url in
                chatLogViewModel.textMessage = "/img \(url)"
                chatLogViewModel.sendMessage()
                chatLogViewModel.isGifPickerVisible = false
            } onShouldDismissGifPicker: {
                chatLogViewModel.isGifPickerVisible = false
            }
            .ignoresSafeArea()
        }

Result IMG_5909

You have full functionality with medium or full size sheet size and also you can use blur theme because our custom sheet wrapper does not have any background.