Skip to content

Commit

Permalink
feat: alamofire 추가해서 성공한 버전 (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
hryeong66 committed Sep 30, 2024
1 parent 70fb038 commit c71105a
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 65 deletions.
114 changes: 81 additions & 33 deletions Projects/Core/DesignSystem/Sources/View/Keyword/KeywordsTagView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import ResourceKit

// thanks to NamS
public struct KeywordsTagView: View {
public let keywordTags: [KeywordTag]
let keywordTags: [KeywordTag]
var onTapHandler: ((String) -> ())?

public init(keywordTags: [KeywordTag], onTapHandler: ((String) -> ())?) {
self.keywordTags = keywordTags
self.onTapHandler = onTapHandler
}

public var body: some View {
ScrollView {
CategoryTagLayout(verticalSpacing: 8, horizontalSpacing: 8) {
FlowLayout(spacing: 8, lineSpacing: 8) {
ForEach(keywordTags, id: \.self) { keywordTag in
Text(keywordTag.name)
.font(Font.Body.Medium.medium)
Expand All @@ -47,47 +47,95 @@ public struct KeywordsTagView: View {
}
}

struct CategoryTagLayout: Layout {
var verticalSpacing: CGFloat = 0
var horizontalSpacing: CGFloat = 0
struct FlowLayout: Layout {
var spacing: CGFloat?
var lineSpacing: CGFloat

func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize {
CGSize(width: proposal.width ?? 0, height: cache.height)
init(spacing: CGFloat? = nil, lineSpacing: CGFloat) {
self.spacing = spacing
self.lineSpacing = lineSpacing
}

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) {
var sumX: CGFloat = bounds.minX
var sumY: CGFloat = bounds.minY
struct Cache {
var sizes: [CGSize] = []
var spacing: [CGFloat] = []
}

func makeCache(subviews: Subviews) -> Cache {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
let spacing: [CGFloat] = subviews.indices.map { index in
guard index != subviews.count - 1 else {
return 0
}

return subviews[index].spacing.distance(
to: subviews[index+1].spacing,
along: .horizontal
)
}

return Cache(sizes: sizes, spacing: spacing)
}

func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) -> CGSize {
var totalHeight = 0.0
var totalWidth = 0.0

var lineWidth = 0.0
var lineHeight = 0.0

for index in subviews.indices {
let view = subviews[index]
let viewSize = view.sizeThatFits(.unspecified)
guard let proposalWidth = proposal.width else { continue }

if (sumX + viewSize.width > proposalWidth) {
sumX = bounds.minX
sumY += viewSize.height
sumY += verticalSpacing
if lineWidth + cache.sizes[index].width > proposal.width ?? 0 {
totalHeight += lineHeight + lineSpacing // 줄 간격 추가
lineWidth = cache.sizes[index].width
lineHeight = cache.sizes[index].height
} else {
lineWidth += cache.sizes[index].width + (spacing ?? cache.spacing[index])
lineHeight = max(lineHeight, cache.sizes[index].height)
}

let point = CGPoint(x: sumX, y: sumY)
view.place(at: point, anchor: .topLeading, proposal: proposal)
sumX += viewSize.width
sumX += horizontalSpacing
totalWidth = max(totalWidth, lineWidth)
}

if let firstViewSize = subviews.first?.sizeThatFits(.unspecified) {
cache.height = sumY + firstViewSize.height
}
}
totalHeight += lineHeight

struct Cache {
var height: CGFloat
return .init(width: totalWidth, height: totalHeight)
}

func makeCache(subviews: Subviews) -> Cache {
return Cache(height: 0)
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout Cache
) {
var lineX = bounds.minX
var lineY = bounds.minY
var lineHeight: CGFloat = 0

for index in subviews.indices {
if lineX + cache.sizes[index].width > (proposal.width ?? 0) {
lineY += lineHeight + lineSpacing // 줄 간격 추가
lineHeight = 0
lineX = bounds.minX
}

let position = CGPoint(
x: lineX + cache.sizes[index].width / 2,
y: lineY + cache.sizes[index].height / 2
)

lineHeight = max(lineHeight, cache.sizes[index].height)
lineX += cache.sizes[index].width + (spacing ?? cache.spacing[index])

subviews[index].place(
at: position,
anchor: .center,
proposal: ProposedViewSize(cache.sizes[index])
)
}
}

func updateCache(_ cache: inout Cache, subviews: Subviews) { }
}
42 changes: 38 additions & 4 deletions Projects/Core/PPACData/Sources/Endpoint/MemeEditEndPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import Foundation
import PPACNetwork
import UIKit
import Alamofire

enum MemeEditEndPoint: MultipartRequestable {
case registerMeme(formData: FormData, title: String, source: String, keywordIds: [String])

var httpMethod: HTTPMethod {
var httpMethod: PPACNetwork.HTTPMethod {
switch self {
case .registerMeme:
return .post
Expand All @@ -34,20 +35,53 @@ enum MemeEditEndPoint: MultipartRequestable {
return nil
}

var formData: MultipartFormData {
var formData: PPACNetwork.MultipartFormData {
switch self {
case .registerMeme(let formData, let title, let source, let keywordIds):
let formFields: [String : String] = ["title" : title,
"source": source]
print("=============== MultipartFormData ===============\n")
var multipartFormData = MultipartFormData(formFields: formFields, formData: formData)
var multipartFormData = PPACNetwork.MultipartFormData()

formFields.forEach { key, value in
multipartFormData.appendTextField(named: key, value: value)
}

for keyword in keywordIds {
multipartFormData.body.append(multipartFormData.appendTextField(named: "keywordIds[]", value: keyword))
multipartFormData.appendTextField(named: "keywordIds[]", value: keyword)
}

multipartFormData.appendFormData(formData: formData)
multipartFormData.appendFinalBoundary()
print("=============== END ===============")
return multipartFormData
}
}

var multipartFormData: Alamofire.MultipartFormData {
switch self {
case .registerMeme(let formData, let title, let source, let keywordIds):

let formFields: [String : String] = ["title" : title,
"source": source]
let multipartFormData = Alamofire.MultipartFormData()

formFields.forEach { key, value in
if let data = "\(value)".data(using: .utf8) {
multipartFormData.append(data, withName: key)
}
}

keywordIds.forEach { keywordId in
if let data = "\(keywordId)".data(using: .utf8) {
multipartFormData.append(data, withName: "keywordIds[]")
}
}

multipartFormData.append(formData.fileData, withName: formData.fieldName, fileName: formData.fileName, mimeType: formData.mimeType)

return multipartFormData

}
}
}
3 changes: 2 additions & 1 deletion Projects/Core/PPACNetwork/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ let project = Project(
sources: "Sources/**",
resources: "Resources/**",
dependencies: [
.Core.PPACUtil
.Core.PPACUtil,
.ThirdParty.Alamofire
]
)
]
Expand Down
22 changes: 8 additions & 14 deletions Projects/Core/PPACNetwork/Sources/MultipartFormData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,10 @@ public struct MultipartFormData {
public init(
boundary: String = UUID().uuidString,
formFields: FormField = [:],
formData: FormData
formData: FormData? = nil
) {
self.boundary = boundary
self.boundaryGenerator = BoundaryGenerator(boundary: boundary)

formFields.forEach {
self.body.append(appendTextField(named: $0.key, value: $0.value))
}

self.body.append(appendFormData(formData: formData))
}

public var contentType: String {
Expand All @@ -98,30 +92,30 @@ public struct MultipartFormData {
return body
}

public func appendTextField(named name: String, value: String) -> Data {
public mutating func appendTextField(named name: String, value: String) {
var data = Data()
data.append(boundaryGenerator.boundaryData(forBoundaryType: .encapsulated))
data.append("Content-Disposition: form-data; name=\"\(name)\"\(EncodingCharacters.crlf)")
data.append("Content-Disposition: form-data; name=\"\(name)\"\(EncodingCharacters.crlf)\(EncodingCharacters.crlf)")
data.append("\(value)\(EncodingCharacters.crlf)")
return data
self.body.append(data)
}

public func appendFormData(formData: FormData) -> Data {
public mutating func appendFormData(formData: FormData) {
var data = Data()
data.append(boundaryGenerator.boundaryData(forBoundaryType: .encapsulated))
data.append("Content-Disposition: form-data; name=\"\(formData.fieldName)\"; filename=\"\(formData.fileName)\"\(EncodingCharacters.crlf)")
data.append("Content-Type: \(formData.mimeType)\(EncodingCharacters.crlf)\(EncodingCharacters.crlf)")
data.append(formData.fileData)
debugPrint(formData.fileData)
print(formData.fileData)
data.append(EncodingCharacters.crlf)
return data
self.body.append(data)
}
}

extension Data {
mutating func append(_ string: String) {
if let data = string.data(using: .utf8) {
debugPrint(string)
print(string)
self.append(data)
}
}
Expand Down
2 changes: 2 additions & 0 deletions Projects/Core/PPACNetwork/Sources/MultipartRequestable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
//

import Foundation
import Alamofire

public protocol MultipartRequestable: Requestable {
var formData: MultipartFormData { get }
var multipartFormData: Alamofire.MultipartFormData { get }
}
42 changes: 41 additions & 1 deletion Projects/Core/PPACNetwork/Sources/NetworkService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import Alamofire

final public class NetworkService: NetworkServiceable {

Expand All @@ -18,10 +19,12 @@ final public class NetworkService: NetworkServiceable {
}

var urlRequest = request.buildURLRequest(with: url)


if let multipartRequest = request as? MultipartRequestable {
let result = await self.excuteAFUploadRequest(multipartRequest, dataType: dataType)
print("result = \(result)")
let multipartFormData = multipartRequest.formData
// urlRequest.setValue("gzip, deflate, br", forHTTPHeaderField: "accept-encoding")
urlRequest.setValue("*/*", forHTTPHeaderField: "Accept")
urlRequest.setValue(multipartFormData.contentType, forHTTPHeaderField: "Content-Type")
return await executeUploadRequest(urlRequest, multipartFormData.finalize(), dataType: dataType)
Expand Down Expand Up @@ -52,6 +55,43 @@ final public class NetworkService: NetworkServiceable {
}
}

private func excuteAFUploadRequest<T: Decodable>(_ request: MultipartRequestable, dataType: T.Type) async -> Result<T, NetworkError> {
guard let url = request.makeURL() else {
NetworkLogger.logError(.urlEncodingError)
return .failure(.urlEncodingError)
}

var urlRequest = request.buildURLRequest(with: url)
urlRequest.setValue("*/*", forHTTPHeaderField: "Accept")
urlRequest.setValue("multipart/form-data", forHTTPHeaderField: "Content-Type")
let headers = urlRequest.allHTTPHeaderFields ?? [:]
do {
return try await withCheckedThrowingContinuation { continuation in
AF.upload(
multipartFormData: request.multipartFormData,
to: url,
method: .post,
headers: HTTPHeaders(headers))
.responseJSON { response in
switch response.result {
case .success(let data):
do {
guard let responseData = response.data else { return }
let decodedData = try JSONDecoder().decode(T.self, from: responseData)
continuation.resume(returning: .success(decodedData))
} catch {
continuation.resume(returning: .failure(.dataDecodingError))
}
case .failure(let error):
continuation.resume(returning: .failure(NetworkError.serverError(statusCode: error.responseCode ?? -1 , message: error.errorDescription)))
}
}
}
} catch {
return .failure(.invalidResponse)
}
}

private func handleResponse<T: Decodable>(_ response: URLResponse, data: Data, dataType: T.Type) -> Result<T, NetworkError> {
guard let httpResponse = response as? HTTPURLResponse else {
NetworkLogger.logError(.invalidResponse)
Expand Down
12 changes: 9 additions & 3 deletions Projects/Features/MemeDetail/Sources/MemeDetailViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,14 @@ private extension MemeDetailViewModel {

@MainActor
func showShareSheet() async {
let deeplinkUrl = "https://farmeme.onelink.me/RtpU/y09dosru?deep_link_value=\(self.state.meme.id)"
self.router?.showShareView(items: [deeplinkUrl])
self.logMemeDetail(event: .share)
do {
let deeplinkUrl = "https://farmeme.onelink.me/RtpU/y09dosru?deep_link_value=\(self.state.meme.id)"
self.router?.showShareView(items: [deeplinkUrl])
try await self.shareMemeUseCase.execute(memeId: state.meme.id)
self.logMemeDetail(event: .share)
} catch {
// TODO: - 에러처리
print(error)
}
}
}
Loading

0 comments on commit c71105a

Please sign in to comment.