Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Custom WebView 작성 및 토큰 재발급 로직 작성, MDS 컬러 마이페이지 하위에 적용 #309

4 changes: 4 additions & 0 deletions SOPT-iOS/Projects/Core/Sources/Literals/StringLiterals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,10 @@ public struct I18N {
public static let expiredLinkDesription = "해당 링크의 유효기간이 만료되어\n더 이상 내용을 확인할 수 없어요."
public static let updateAlertButtonTitle = "확인"
}

public struct WebView {
public static let close = "닫기"
}
}

extension I18N {
Expand Down
5 changes: 5 additions & 0 deletions SOPT-iOS/Projects/Demo/Sources/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import Sentry

import Networks
import Core
import BaseFeatureDependency

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

private var appLifecycleAdapter = AppLifecycleAdapter()

func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
configureSentry()
registerDependencies()
application.registerForRemoteNotifications()

appLifecycleAdapter.prepare()
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public final class MyPageSectionListItemView: UIView {
private lazy var contentStackView = UIStackView().then {
$0.axis = .horizontal
$0.distribution = .fill
$0.backgroundColor = DSKitAsset.Colors.black80.color
}

private let titleLabel = UILabel().then {
Expand Down Expand Up @@ -85,7 +84,7 @@ public final class MyPageSectionListItemView: UIView {
self.rightSwitch.isHidden = false
}

self.backgroundColor = DSKitAsset.Colors.black80.color
self.backgroundColor = DSKitAsset.Colors.gray900.color

self.setupLayouts()
self.setupConstraint()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public final class MypageSectionGroupView: UIView {
}

private let headerView = UIView().then {
$0.backgroundColor = DSKitAsset.Colors.black80.color
$0.backgroundColor = DSKitAsset.Colors.gray900.color
$0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
$0.layer.cornerRadius = Metric.sectionGroupCornerRadius
}
Expand All @@ -44,7 +44,7 @@ public final class MypageSectionGroupView: UIView {
}

private let bottomInsetView = UIView().then {
$0.backgroundColor = DSKitAsset.Colors.black80.color
$0.backgroundColor = DSKitAsset.Colors.gray900.color
$0.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
$0.layer.cornerRadius = Metric.sectionGroupCornerRadius
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension AlertVC.AlertTheme {
var backgroundColor: UIColor {
switch self {
case .main:
return DSKitAsset.Colors.black60.color
return DSKitAsset.Colors.gray700.color
case .soptamp:
return DSKitAsset.Colors.white.color
}
Expand Down Expand Up @@ -66,7 +66,7 @@ extension AlertVC.AlertTheme {
func cancelButtonColor(isNetworkErr: Bool) -> UIColor {
switch self {
case .main:
return isNetworkErr ? DSKitAsset.Colors.white100.color : DSKitAsset.Colors.black40.color
return isNetworkErr ? DSKitAsset.Colors.white100.color : DSKitAsset.Colors.gray600.color
case .soptamp:
return isNetworkErr ? DSKitAsset.Colors.soptampError200.color : DSKitAsset.Colors.soptampGray300.color
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// AppLifecycleAdapter.swift
// BaseFeatureDependency
//
// Created by Ian on 12/2/23.
// Copyright © 2023 SOPT-iOS. All rights reserved.
//

import Core
import Networks

import UIKit

final public class AppLifecycleAdapter {
private let authService: AuthService
private let cancelBag = CancelBag()

public init() {
self.authService = DefaultAuthService()
}
}

// MARK: - Public functions
extension AppLifecycleAdapter {
public func prepare() {
self.onWillEnterForeground()
self.onWillEnterBackground()
}
}

// MARK: - Lifecycle Usecases
extension AppLifecycleAdapter {
private func onWillEnterForeground() {
NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] _ in
self?.reissureTokens()
}).store(in: self.cancelBag)
}

private func onWillEnterBackground() { }
}

// MARK: - Private functions
extension AppLifecycleAdapter {
private func reissureTokens() {
guard UserDefaultKeyList.Auth.appAccessToken != nil else { return }

self.authService.reissuance { _ in }
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public protocol RouterProtocol: ViewControllable {
func present(_ module: ViewControllable?, animated: Bool)
func present(_ module: ViewControllable?, animated: Bool, modalPresentationSytle: UIModalPresentationStyle)
func present(_ module: ViewControllable?, animated: Bool, completion: (() -> Void)?)
func pushSOPTWebView(url: String)
func presentSafari(url: String)

func push(_ module: ViewControllable?)
Expand Down Expand Up @@ -111,6 +112,13 @@ final class Router: NSObject, RouterProtocol {
self.rootController?.present(controller, animated: animated, completion: completion)
}

public func pushSOPTWebView(url: String) {
guard let url = URL(string: url) else { return }

let webView = SOPTWebView(startWith: url)
self.rootController?.pushViewController(webView, animated: true)
}

public func presentSafari(url: String) {
let safariViewController = SFSafariViewController(url: URL(string: url)!)
safariViewController.playgroundStyle()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// SOPTWebView.swift
// BaseFeatureDependency
//
// Created by Ian on 11/30/23.
// Copyright © 2023 SOPT-iOS. All rights reserved.
//

import Core
import DSKit

import UIKit
import WebKit

import SnapKit

public final class SOPTWebView: UIViewController {
private enum Metric {
static let navigationBarHeight = 44.f
}

private lazy var navigationBar = WebViewNavigationBar(frame: self.view.frame)
private let wkwebView: WKWebView

// MARK: Variables
private let cancelbag = CancelBag()
private var barrier = false

public init(
config: WebViewConfig = WebViewConfig(),
startWith url: URL
) {
let configuration = WKWebViewConfiguration().then {
$0.allowsInlineMediaPlayback = config.allowsInlineMediaPlayback
$0.mediaTypesRequiringUserActionForPlayback = config.mediaTypesRequiringUserActionForPlayback
}

self.wkwebView = WKWebView(frame: .zero, configuration: configuration).then {
$0.allowsBackForwardNavigationGestures = config.allowsBackForwardNavigationGestures
}

super.init(nibName: nil, bundle: nil)

DispatchQueue.main.async {
let request = URLRequest(url: url)
self.wkwebView.load(request)
}
}

public required init?(coder: NSCoder) {
fatalError("coder initializer doesn't implemented.")
}

public override func viewDidLoad() {
super.viewDidLoad()

self.view.backgroundColor = DSKitAsset.Colors.black100.color

self.wkwebView.scrollView.delegate = self
self.wkwebView.navigationDelegate = self
self.wkwebView.uiDelegate = self

self.initializeViews()
self.setupConstraints()
self.setupNavigationButtonActions()
}
}

extension SOPTWebView {
private func initializeViews() {
self.view.addSubviews(self.navigationBar, self.wkwebView)
}

private func setupConstraints() {
self.navigationBar.snp.makeConstraints {
$0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
$0.leading.trailing.equalToSuperview()
$0.height.equalTo(Metric.navigationBarHeight)
}

self.wkwebView.snp.makeConstraints {
$0.top.equalTo(self.navigationBar.snp.bottom)
$0.leading.trailing.bottom.equalToSuperview()
}
}

private func setupNavigationButtonActions() {
self.navigationBar
.signalForClickLeftButton()
.sink { [weak self] _ in
guard self?.wkwebView.canGoBack == true else {
self?.navigationController?.popViewController(animated: true)
return
}

self?.wkwebView.goBack()
}.store(in: self.cancelbag)

self.navigationBar
.signalForClickRightButton()
.sink { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
}.store(in: self.cancelbag)
}
}

extension SOPTWebView: WKNavigationDelegate {
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard !self.barrier else { return }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

barrier는 didFinish에 구현된 내용(토큰 주입)이 1회만 발생하기 위해 플래그를 생성한 걸까요~?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요~ 리액트가 실행되는 타이밍이 제각각이고 무한 리로드가 되는 경우가 생겨서 1회만 되도록 방지해 두었어요


self.barrier = true
self.wkwebView.evaluateJavaScript(
"localStorage.setItem(\"serviceAccessToken\", \"\(UserDefaultKeyList.Auth.playgroundToken!)\")"
)
self.wkwebView.reload()
}
}

extension SOPTWebView: WKUIDelegate {
public func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
}

return nil
}
}

extension SOPTWebView: UIScrollViewDelegate {
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
scrollView.pinchGestureRecognizer?.isEnabled = false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// WebViewConfiguration.swift
// BaseFeatureDependency
//
// Created by Ian on 11/30/23.
// Copyright © 2023 SOPT-iOS. All rights reserved.
//

import WebKit

public struct WebViewConfig {
let javaScriptEnabled: Bool
let allowsBackForwardNavigationGestures: Bool
let allowsInlineMediaPlayback: Bool
let mediaTypesRequiringUserActionForPlayback: WKAudiovisualMediaTypes
let isScrollEnabled: Bool

public init(
javaScriptEnabled: Bool = true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wkwebview javaScriptEnabled 속성에 대응 - 미사용

allowsBackForwardNavigationGestures: Bool = true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

navigation backswipe가 가능하도록 하는 프로퍼티. wkwebview allowsBackForward~ 에 대응

allowsInlineMediaPlayback: Bool = true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default : false. false인 경우 동영상이 있으면 전체 화면으로 재생 됨. inline으로 재생할 수 있도록 하는 프로퍼티.

mediaTypesRequiringUserActionForPlayback: WKAudiovisualMediaTypes = [],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자 제스쳐가 필요한 미디어 타입. 없어도 됨

isScrollEnabled: Bool = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

웹뷰의 스크롤 가능 여부.

) {
self.javaScriptEnabled = javaScriptEnabled
self.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures
self.allowsInlineMediaPlayback = allowsInlineMediaPlayback
self.mediaTypesRequiringUserActionForPlayback = mediaTypesRequiringUserActionForPlayback
self.isScrollEnabled = isScrollEnabled
}
}
Loading