feat: add compose view

This commit is contained in:
CMK 2021-07-16 21:21:18 +08:00
parent 7804f679c5
commit 079e611f33
13 changed files with 399 additions and 43 deletions

View File

@ -491,6 +491,8 @@
DBC6462926A1736700B0E31B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; };
DBC6462B26A1738900B0E31B /* MastodonUI in Frameworks */ = {isa = PBXBuildFile; productRef = DBC6462A26A1738900B0E31B /* MastodonUI */; };
DBC6462C26A176B000B0E31B /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; };
DBC6463326A195DB00B0E31B /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; };
DBC6463726A195DB00B0E31B /* CoreDataStack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */; };
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; };
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */; };
DBCBCBF4267CB070000F5B51 /* Decode85.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCBCBF3267CB070000F5B51 /* Decode85.swift */; };
@ -615,6 +617,20 @@
remoteGlobalIDString = DBC6461126A170AB00B0E31B;
remoteInfo = ShareActionExtension;
};
DBC6463526A195DB00B0E31B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DB427DCA25BAA00100D1B89D /* Project object */;
proxyType = 1;
remoteGlobalIDString = DB68047E2637CD4C00430867;
remoteInfo = AppShared;
};
DBC6463926A195DB00B0E31B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DB427DCA25BAA00100D1B89D /* Project object */;
proxyType = 1;
remoteGlobalIDString = DB89B9ED25C10FD0008580ED;
remoteInfo = CoreDataStack;
};
DBF8AE18263293E400C9C23C /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DB427DCA25BAA00100D1B89D /* Project object */;
@ -1281,6 +1297,8 @@
buildActionMask = 2147483647;
files = (
DBC6462526A1720B00B0E31B /* MastodonUI in Frameworks */,
DBC6463726A195DB00B0E31B /* CoreDataStack.framework in Frameworks */,
DBC6463326A195DB00B0E31B /* AppShared.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -3012,6 +3030,8 @@
buildRules = (
);
dependencies = (
DBC6463626A195DB00B0E31B /* PBXTargetDependency */,
DBC6463A26A195DB00B0E31B /* PBXTargetDependency */,
);
name = ShareActionExtension;
packageProductDependencies = (
@ -3955,6 +3975,16 @@
target = DBC6461126A170AB00B0E31B /* ShareActionExtension */;
targetProxy = DBC6461A26A170AB00B0E31B /* PBXContainerItemProxy */;
};
DBC6463626A195DB00B0E31B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DB68047E2637CD4C00430867 /* AppShared */;
targetProxy = DBC6463526A195DB00B0E31B /* PBXContainerItemProxy */;
};
DBC6463A26A195DB00B0E31B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DB89B9ED25C10FD0008580ED /* CoreDataStack */;
targetProxy = DBC6463926A195DB00B0E31B /* PBXContainerItemProxy */;
};
DBF8AE19263293E400C9C23C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = DBF8AE12263293E400C9C23C /* NotificationService */;
@ -4434,14 +4464,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.9.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -4454,14 +4485,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.9.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -4474,14 +4506,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.9.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -4494,14 +4527,15 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 5Z4GVSS33P;
INFOPLIST_FILE = ShareActionExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 0.9.0;
PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.ShareActionExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;

View File

@ -12,37 +12,37 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>21</integer>
<integer>20</integer>
</dict>
<key>Mastodon - ASDK.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>5</integer>
</dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
<integer>7</integer>
</dict>
<key>Mastodon - Release.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>3</integer>
</dict>
<key>Mastodon.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>22</integer>
<integer>21</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>30</integer>
<integer>22</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -105,8 +105,8 @@
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
"state": {
"branch": null,
"revision": "15d199e84677303a7004ed2c5ecaa1a90f3863f8",
"version": "6.2.1"
"revision": "44450a8f564d7c0165f736ba2250649ff8d3e556",
"version": "6.3.0"
}
},
{
@ -123,8 +123,8 @@
"repositoryURL": "https://github.com/kean/Nuke.git",
"state": {
"branch": null,
"revision": "69ae6d5b8c4b898450432f94bd35f863d3830cfc",
"version": "10.3.0"
"revision": "83e1edaa5a30c567eb129c21c6d00f2f552d2c6f",
"version": "10.3.1"
}
},
{

View File

@ -5,13 +5,15 @@
// Created by MainasuK Cirno on 2021-2-5.
//
#if DEBUG
import os.log
import UIKit
import CoreData
import CoreDataStack
#if DEBUG
import FLEX
import SwiftUI
import MastodonUI
extension HomeTimelineViewController {
var debugMenu: UIMenu {
@ -55,6 +57,10 @@ extension HomeTimelineViewController {
guard let self = self else { return }
self.showThreadAction(action)
},
UIAction(title: "Show Share Action Compose", image: UIImage(systemName: "square.and.arrow.up"), attributes: []) { [weak self] action in
guard let self = self else { return }
self.showShareActionExtensionComposeView(action)
},
UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in
guard let self = self else { return }
self.showSettings(action)
@ -363,5 +369,14 @@ extension HomeTimelineViewController {
transition: .modal(animated: true, completion: nil)
)
}
@objc private func showShareActionExtensionComposeView(_ sender: UIAction) {
let viewController = UIHostingController(
rootView: ComposeView().environmentObject(MastodonUI.ComposeViewModel())
)
let navigationController = UINavigationController(rootViewController: viewController)
present(navigationController, animated: true, completion: nil)
}
}
#endif

View File

@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "MastodonSDK",
platforms: [
.iOS(.v13),
.iOS(.v14),
],
products: [
.library(
@ -22,6 +22,8 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
.package(url: "https://github.com/kean/Nuke.git", from: "10.3.1"),
.package(name: "NukeFLAnimatedImagePlugin", url: "https://github.com/kean/Nuke-FLAnimatedImage-Plugin.git", from: "8.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@ -35,7 +37,11 @@ let package = Package(
),
.target(
name: "MastodonUI",
dependencies: ["MastodonExtension"]
dependencies: [
"MastodonExtension",
"Nuke",
"NukeFLAnimatedImagePlugin"
]
),
.target(
name: "MastodonExtension",

View File

@ -0,0 +1,56 @@
//
// AnimatedImage.swift
//
//
// Created by MainasuK Cirno on 2021-7-16.
//
import SwiftUI
import Nuke
import FLAnimatedImage
struct AnimatedImage: UIViewRepresentable {
let imageURL: URL?
func makeUIView(context: Context) -> FLAnimatedImageViewProxy {
let proxy = FLAnimatedImageViewProxy(frame: .zero)
Nuke.loadImage(with: imageURL, into: proxy.imageView)
return proxy
}
func updateUIView(_ proxy: FLAnimatedImageViewProxy, context: Context) {
Nuke.cancelRequest(for: proxy.imageView)
Nuke.loadImage(with: imageURL, into: proxy.imageView)
}
}
final class FLAnimatedImageViewProxy: UIView {
let imageView = FLAnimatedImageView()
override init(frame: CGRect) {
super.init(frame: frame)
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct AnimatedImage_Previews: PreviewProvider {
static var previews: some View {
AnimatedImage(
imageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
)
.frame(width: 300, height: 300)
}
}

View File

@ -0,0 +1,68 @@
//
// ComposeView.swift
//
//
// Created by MainasuK Cirno on 2021-7-16.
//
import SwiftUI
public struct ComposeView: View {
@EnvironmentObject public var viewModel: ComposeViewModel
public init() { }
public var body: some View {
GeometryReader { proxy in
ScrollView(.vertical) {
StatusAuthorView(
avatarImageURL: viewModel.avatarImageURL,
name: viewModel.authorName,
username: viewModel.authorUsername
)
TextEditorView(
string: $viewModel.statusContent,
width: viewModel.frame.width,
attributedString: viewModel.statusContentAttributedString
)
.frame(width: viewModel.frame.width)
.frame(minHeight: 100)
ForEach(viewModel.attachments, id: \.self) { image in
Image(uiImage: image)
.resizable()
.aspectRatio(16.0/9.0, contentMode: .fill)
.frame(maxWidth: .infinity)
.background(Color.gray)
.cornerRadius(4)
}
} // end ScrollView
.preference(
key: ComposeViewFramePreferenceKey.self,
value: proxy.frame(in: .local)
)
.onPreferenceChange(ComposeViewFramePreferenceKey.self) { frame in
viewModel.frame = frame
print(frame)
}
}
}
}
struct ComposeViewFramePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) { }
}
struct ComposeView_Previews: PreviewProvider {
static let viewModel: ComposeViewModel = {
let viewModel = ComposeViewModel()
return viewModel
}()
static var previews: some View {
ComposeView().environmentObject(viewModel)
}
}

View File

@ -0,0 +1,46 @@
//
// ComposeViewModel.swift
// ShareActionExtension
//
// Created by MainasuK Cirno on 2021-7-16.
//
import Foundation
import SwiftUI
import Combine
public class ComposeViewModel: ObservableObject {
var disposeBag = Set<AnyCancellable>()
@Published var frame: CGRect = .zero
@Published var avatarImageURL: URL?
@Published var authorName: String = ""
@Published var authorUsername: String = ""
@Published var statusContent = ""
@Published var statusContentAttributedString = NSAttributedString()
@Published var contentWarningContent = ""
@Published var attachments: [UIImage] = []
public init() {
$statusContent
.map { NSAttributedString(string: $0) }
.assign(to: &$statusContentAttributedString)
#if DEBUG
avatarImageURL = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")
authorName = "Alice"
authorUsername = "alice"
attachments = [
UIImage(systemName: "photo")!,
UIImage(systemName: "photo")!,
UIImage(systemName: "photo")!,
UIImage(systemName: "photo")!,
]
#endif
}
}

View File

@ -0,0 +1,44 @@
//
// StatusAuthorView.swift
//
//
// Created by MainasuK Cirno on 2021-7-16.
//
import SwiftUI
import Nuke
import NukeFLAnimatedImagePlugin
import FLAnimatedImage
struct StatusAuthorView: View {
let avatarImageURL: URL?
let name: String
let username: String
var body: some View {
HStack(spacing: 5) {
AnimatedImage(imageURL: avatarImageURL)
.frame(width: 42, height: 42)
.cornerRadius(4)
VStack(alignment: .leading) {
Text(name)
.font(.headline)
Text("@" + username)
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
}
}
struct StatusAuthorView_Previews: PreviewProvider {
static var previews: some View {
StatusAuthorView(
avatarImageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"),
name: "Alice",
username: "alice"
)
}
}

View File

@ -0,0 +1,80 @@
//
// TextEditorView.swift
//
//
// Created by MainasuK Cirno on 2021-7-16.
//
import UIKit
import SwiftUI
public struct TextEditorView: UIViewRepresentable {
@Binding var string: String
let width: CGFloat
let attributedString: NSAttributedString
public init(
string: Binding<String>,
width: CGFloat,
attributedString: NSAttributedString
) {
self._string = string
self.width = width
self.attributedString = attributedString
}
public func makeUIView(context: Context) -> UITextView {
let textView = UITextView(frame: .zero)
textView.isScrollEnabled = false
textView.font = .preferredFont(forTextStyle: .body)
textView.textColor = .label
textView.delegate = context.coordinator
textView.translatesAutoresizingMaskIntoConstraints = false
let widthLayoutConstraint = textView.widthAnchor.constraint(equalToConstant: 100)
widthLayoutConstraint.priority = .required - 1
context.coordinator.widthLayoutConstraint = widthLayoutConstraint
return textView
}
public func updateUIView(_ textView: UITextView, context: Context) {
// update content
// textView.attributedText = attributedString
textView.text = string
// update layout
context.coordinator.updateLayout(width: width)
}
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public class Coordinator: NSObject, UITextViewDelegate {
var parent: TextEditorView
var widthLayoutConstraint: NSLayoutConstraint?
init(_ parent: TextEditorView) {
self.parent = parent
}
public func textViewDidChange(_ textView: UITextView) {
parent.string = textView.text
}
func updateLayout(width: CGFloat) {
guard let widthLayoutConstraint = widthLayoutConstraint else { return }
widthLayoutConstraint.constant = width
widthLayoutConstraint.isActive = true
}
}
}

View File

@ -17,24 +17,24 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>4</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>4</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import Combine
import MastodonUI
import SwiftUI
class ShareViewController: UIViewController {
@ -45,19 +46,8 @@ class ShareViewController: UIViewController {
return barButtonItem
}()
// let tableView: ComposeTableView = {
// let tableView = ComposeTableView()
// tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self))
// tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
// tableView.alwaysBounceVertical = true
// tableView.separatorStyle = .none
// tableView.tableFooterView = UIView()
// return tableView
// }()
}
extension ShareViewController {
override func viewDidLoad() {
@ -74,6 +64,21 @@ extension ShareViewController {
}
.store(in: &disposeBag)
let hostingViewController = UIHostingController(
rootView: ComposeView().environmentObject(viewModel.composeViewModel)
)
addChild(hostingViewController)
view.addSubview(hostingViewController.view)
hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingViewController.view)
NSLayoutConstraint.activate([
hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
hostingViewController.didMove(toParent: self)
// viewModel.authentication
// .receive(on: DispatchQueue.main)
// .sink { [weak self] result in

View File

@ -10,6 +10,7 @@ import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonUI
final class ShareViewModel {
@ -27,6 +28,7 @@ final class ShareViewModel {
let isFetchAuthentication = CurrentValueSubject<Bool, Never>(true)
let isBusy = CurrentValueSubject<Bool, Never>(true)
let isValid = CurrentValueSubject<Bool, Never>(false)
let composeViewModel = ComposeViewModel()
init() {
viewDidAppear.receive(on: DispatchQueue.main)