I have question at the top of my view that looks something along the lines of "This is a _____ question"
and then I have choice buttons below the question
[choice1] [choice2] [choice3] [example]
Whenever the user clicks [example] I want it to move to the blank space in the question.
Are you able to help me implement .matchedGeometryEffect to achieve this?
Here is the main view, I am implementing WrappingHStack package
import SwiftUI
import WrappingHStack
struct MyTextPreferenceKey2: PreferenceKey {
typealias Value = [MyTextPreferenceData2]
static var defaultValue: [MyTextPreferenceData2] = []
static func reduce(value: inout [MyTextPreferenceData2], nextValue: () -> [MyTextPreferenceData2]) {
value.append(contentsOf: nextValue())
}
}
struct MyTextPreferenceData2: Equatable {
let viewIdx: Int
let rect: CGRect
}
struct ShortStoryPlugInQuestionsView: View {
@ObservedObject var shortStoryPlugInVM: ShortStoryViewModel
@State private var activeIdx: Int = 0
@State private var rects: [CGRect] = Array<CGRect>(repeating: CGRect(), count: 12)
@State var characters: [pluginShortStoryCharacter] = [
pluginShortStoryCharacter(value: "Lorem", isCorrect: false),
pluginShortStoryCharacter(value: "nasce", isCorrect: true),
pluginShortStoryCharacter(value: "is", isCorrect: false),
pluginShortStoryCharacter(value: "simply", isCorrect: false),
pluginShortStoryCharacter(value: "dummy", isCorrect: false),
pluginShortStoryCharacter(value: "text", isCorrect: false),
pluginShortStoryCharacter(value: "if", isCorrect: false),
pluginShortStoryCharacter(value: "the", isCorrect: false),
pluginShortStoryCharacter(value: "design", isCorrect: false),
]
//for drag
@State var shuffledRows: [[pluginShortStoryCharacter]] = []
//for drop
@State var rows: [[pluginShortStoryCharacter]] = []
@State private var frames: [CGRect] = [CGRect]()
@State private var correctAnswers: [String] = ["nasce"]
@State private var animateCorrect = false
@Namespace private var namespace
var body: some View {
GeometryReader { geo in
ZStack{
VStack(spacing: 0){
ScrollViewReader {scrollView in
ScrollView(.horizontal){
HStack{
ForEach(0..<shortStoryPlugInVM.currentPlugInQuestions.count, id: \.self) {i in
VStack{
VStack{
var sentenceArray: [String] = shortStoryPlugInVM.currentPlugInQuestions[i].question.components(separatedBy: " ")
var missingWord: String = shortStoryPlugInVM.currentPlugInQuestions[i].missingWord
WrappingHStack(0..<sentenceArray.count, id:\.self) {i in
if sentenceArray[i].elementsEqual(missingWord) {
if animateCorrect{
Text("nasce")
.background{
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(.gray)
}
.matchedGeometryEffect(id: "rightAnswer", in: namespace)
}else{
RoundedRectangle(cornerRadius: 10.0)
.fill(Color.teal)
.frame(width: 35, height: 10)
}
}else {
Text(String(sentenceArray[i]))
.padding(3)
}
}.frame(minWidth: 250)
//correctAnswers.append(shortStoryPlugInVM.currentPlugInQuestions[i].missingWord)
}.frame(width: 300, height: 200)
.background(.teal)
DragArea()
}.frame(width: geo.size.width)
.frame(minHeight: geo.size.height)
}
}
}
}
}.onPreferenceChange(MyTextPreferenceKey2.self) { preferences in
for p in preferences {
self.rects[p.viewIdx] = p.rect
}
}
}.onAppear{
shortStoryPlugInVM.setShortStoryData(storyName: "Cristofo Columbo")
if rows.isEmpty{
//First Creating shuffled On
//then normal one
characters = characters.shuffled()
rows = generateGrid()
shuffledRows = generateGrid()
rows = generateGrid()
}
}
.coordinateSpace(name: "myZstack")
}
}
func setFrame(index: Int, frame: CGRect) {
self.frames.append(frame)
}
func combineTextObjects(_ objects: [Text]) -> Text{
return objects[1...].reduce(objects[0], +)
}
@ViewBuilder
func DragArea()->some View {
VStack(spacing: 12){
ForEach(shuffledRows, id: \.self){row in
HStack(spacing:10){
ForEach(row){item in
Text(item.value)
.font(.system(size: item.fontSize))
.padding(.vertical, 5)
.padding(.horizontal, item.padding)
.background{
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(.gray)
}
.opacity(item.isShowing ? 0 : 1)
.background{
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(item.isShowing ? .gray.opacity(0.25) : .clear)
}
// .offset(x: animateCorrect && item.isCorrect ? rects[0].minX : 0, y: animateCorrect && item.isCorrect ? rects[0].minY : 0)
.onTapGesture{
if item.isCorrect {
SoundManager.instance.playSound(sound: .correct)
withAnimation(.easeIn){
animateCorrect = true
}
}else{
SoundManager.instance.playSound(sound: .wrong)
}
}
// .matchedGeometryEffect(id: "rightAnswer", in: namespace)
}
}
}
}
}
func generateGrid()->[[pluginShortStoryCharacter]]{
for item in characters.enumerated() {
let textSize = textSize(character: item.element)
characters[item.offset].textSize = textSize
}
var gridArray: [[pluginShortStoryCharacter]] = []
var tempArray: [pluginShortStoryCharacter] = []
var currentWidth: CGFloat = 0
let totalScreenWidth: CGFloat = UIScreen.main.bounds.width - 30
for character in characters {
currentWidth += character.textSize
if currentWidth < totalScreenWidth{
tempArray.append(character)
}else {
gridArray.append(tempArray)
tempArray = []
currentWidth = character.textSize
tempArray.append(character)
}
}
if !tempArray.isEmpty{
gridArray.append(tempArray)
}
return gridArray
}
func textSize(character: pluginShortStoryCharacter)->CGFloat{
let font = UIFont.systemFont(ofSize: character.fontSize)
let attributes = [NSAttributedString.Key.font : font]
let size = (character.value as NSString).size(withAttributes: attributes)
return size.width + (character.padding * 2) + 15
}
func updateShuffledArray(character: pluginShortStoryCharacter){
for index in shuffledRows.indices{
for subIndex in shuffledRows[index].indices{
if shuffledRows[index][subIndex].id == character.id{
shuffledRows[index][subIndex].isShowing = true
}
}
}
}
}
struct MyPreferenceViewSetter2: View {
let idx: Int
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.white)
.preference(key: MyTextPreferenceKey2.self,
value: [MyTextPreferenceData2(viewIdx: self.idx, rect: geometry.frame(in: .named("myZstack")))])
}
}
}
struct pluginShortStoryCharacter: Identifiable, Hashable, Equatable {
var id = UUID().uuidString
var value: String
var isCorrect: Bool
var padding: CGFloat = 10
var textSize: CGFloat = .zero
var fontSize: CGFloat = 19
var isShowing: Bool = false
}
struct ShortStoryPlugInQuestionsView_Previews: PreviewProvider {
static var shortStoryPlugInVM = ShortStoryViewModel(currentStoryIn: 0)
static var previews: some View {
ShortStoryPlugInQuestionsView(shortStoryPlugInVM: shortStoryPlugInVM)
}
}
Here is the view model
import Foundation
final class ShortStoryViewModel: ObservableObject {
@Published private(set) var currentPlugInStoryData: [shortStoryPlugInDataObj] = [shortStoryPlugInDataObj]()
@Published private(set) var currentPlugInQuestions: [FillInBlankQuestion] = [FillInBlankQuestion]()
@Published private(set) var currentStory: String
init(currentStoryIn: Int){
switch currentStoryIn {
case 0:
currentStory = "Cristofo Columbo"
default:
currentStory = "Cristofo Columbo"
}
}
func setShortStoryData(storyName: String) {
var tempArray: [shortStoryPlugInDataObj] = [shortStoryPlugInDataObj]()
let shortStoryList: [storyObject] = storyObject.allStoryObjects
var chosenStoryObject: storyObject = shortStoryList[0]
let storyString = chosenStoryObject.story
let wordLinks: [WordLink] = chosenStoryObject.wordLinks
let questions: [QuestionsObj] = chosenStoryObject.questionsObjs
let plugInQuestions: [FillInBlankQuestion] = chosenStoryObject.fillInBlankQuestions
currentPlugInQuestions = plugInQuestions
var newObj = shortStoryPlugInDataObj(storyString: storyString, wordLinksArray: wordLinks, questionList: questions, plugInQuestionlist: plugInQuestions)
tempArray.append(newObj)
currentPlugInStoryData = tempArray
}
JSON Manager with object structs
import Foundation
struct verbObject: Codable{
var verb: Verb
var presenteConjList, passatoProssimoConjList, futuroConjList, imperfettoConjList: [String]
var presenteCondizionaleConjList, imperativoConjList: [String]
static let allVerbObject: [verbObject] = Bundle.main.decode(file: "ItalianAppVerbData.json")
}
struct Verb: Codable {
var verbName, verbEngl: String
}
struct storyObject: Codable {
let storyName, story: String
let wordLinks: [WordLink]
let questionsObjs: [QuestionsObj]
let fillInBlankQuestions: [FillInBlankQuestion]
var dragAndDropQuestions: [DragAndDropQuestion]
static let allStoryObjects: [storyObject] = Bundle.main.decode(file: "shortStoryAppData.json")
static let columbo: storyObject = allStoryObjects[0]
}
// MARK: - DragAndDropQuestion
struct DragAndDropQuestion: Codable {
var englishSentence: String
var choices: [String]
}
struct FillInBlankQuestion: Codable {
let englishLine1, question, missingWord: String
let choices: [String]
}
extension Bundle {
func decode<T: Decodable>(file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Could not find \(file) in the project!")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Could not load \(file) in the project!")
}
let decoder = JSONDecoder()
guard let loadedData = try? decoder.decode(T.self, from: data) else {
fatalError("Culd not decode \(file) in the project!")
}
return loadedData
}
}
Using matchedGeometryEffect
is all about matching up ids in a namespace and identifying one source as the target for the view position.
In your case, I think you start off with a blank placeholder for the answer to a question and you want to replace this with the true answer. Instead of trying to pick through your code, I have tried to prepare a working example that you can maybe use to understand the mechanisms. Hope it helps.
struct ContentView: View {
@Namespace private var namespace
@State private var fillingBlank = false
@State private var answer = 0
private func buttonForAnswer(num: Int) -> some View {
Button("Answer \(num)") {
answer = num
withAnimation {
fillingBlank = true
}
}
.buttonStyle(.borderedProminent)
.matchedGeometryEffect(
id: num,
in: namespace,
isSource: answer == num && !fillingBlank
)
.background {
// This is the text that floats to the blank space
Text("Answer \(num)")
.foregroundColor(.primary)
.matchedGeometryEffect(
id: answer == num && fillingBlank ? 0 : num,
in: namespace,
properties: .position,
isSource: false
)
}
}
var body: some View {
VStack(spacing: 30) {
// Question section
HStack {
Text("(question part 1)")
Text("blank space")
.foregroundColor(.secondary.opacity(0.5))
.opacity(fillingBlank ? 0 : 1)
.background(alignment: .bottom) {
VStack {
Divider().background(.primary)
}
}
.matchedGeometryEffect(
id: 0,
in: namespace,
isSource: fillingBlank
)
Text("(question part 2)")
}
// Answer section
Text("Every answer is correct, please pick one!")
.padding(.top, 50)
VStack {
HStack(spacing: 20) {
// The buttons for the answers
ForEach(1...3, id: \.self) { num in
buttonForAnswer(num: num)
}
}
.overlay {
// The reset button
if fillingBlank {
HStack {
Button("Reset") {
withAnimation {
fillingBlank = false
}
}
.buttonStyle(.borderedProminent )
.tint(.orange)
}
.frame(maxWidth: .infinity)
.background(Color(UIColor.systemBackground))
}
}
}
}
}
}